From b3bfd38ddc0d7525896eca9e75b02220594a63f7 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 27 Nov 2025 14:04:33 +0100 Subject: [PATCH 01/60] WPB-21591 [prep] Move Effects to subsystems (#4868) --- libs/brig-types/brig-types.cabal | 10 +- libs/brig-types/default.nix | 2 - libs/brig-types/src/Brig/Types/Instances.hs | 90 ---- .../brig-types/src/Brig/Types/Provider/Tag.hs | 2 +- libs/galley-types/src/Galley/Types/Teams.hs | 17 +- .../wire-api/src/Wire/API/Provider/Service.hs | 50 ++- .../src/Wire/API/Provider/Service/Tag.hs | 129 +++++- .../src/Wire/API/Routes/Internal/Galley.hs | 13 + libs/wire-api/src/Wire/API/Team.hs | 11 + libs/wire-api/src/Wire/API/Team/Member.hs | 10 + .../src/Wire/API/User/Client/Prekey.hs | 7 + libs/wire-subsystems/default.nix | 14 + libs/wire-subsystems/src/Wire/AWS.hs | 151 ++++++- .../Wire/BackgroundJobsRunner/Interpreter.hs | 8 - .../Wire/ConversationSubsystem/Interpreter.hs | 4 - .../src/Wire/GalleyAPIAccess.hs | 27 +- .../src/Wire/GalleyAPIAccess/Rpc.hs | 45 +- .../src/Wire}/LegalHoldStore.hs | 43 +- .../src/Wire/LegalHoldStore/Cassandra.hs | 148 +++++++ .../Wire/LegalHoldStore/Cassandra/Queries.hs | 75 ++++ .../src/Wire/LegalHoldStore/Env.hs | 11 + .../src/Wire/NotificationSubsystem.hs | 15 + .../wire-subsystems/src/Wire/SparAPIAccess.hs | 19 +- .../src/Wire/SparAPIAccess/Rpc.hs | 33 ++ .../src/Wire/StoredConversation.hs | 17 + .../wire-subsystems/src/Wire/TeamJournal.hs | 45 +- .../src/Wire/TeamJournal/Aws.hs | 28 +- .../wire-subsystems/src/Wire}/TeamStore.hs | 74 +--- .../src/Wire/TeamStore/Cassandra.hs | 333 ++++++++++++++ .../src/Wire/TeamStore/Cassandra/Queries.hs | 180 ++++++++ .../wire-subsystems/src/Wire/TeamSubsystem.hs | 5 +- .../src/Wire/TeamSubsystem/GalleyAPI.hs | 8 +- .../src/Wire/TeamSubsystem/Interpreter.hs | 205 +++++++++ .../test/unit/Wire/MiniBackend.hs | 2 +- .../Wire/MockInterpreters/GalleyAPIAccess.hs | 4 +- .../Wire/MockInterpreters/SparAPIAccess.hs | 2 + .../InterpreterSpec.hs | 2 +- .../UserGroupSubsystem/InterpreterSpec.hs | 4 +- libs/wire-subsystems/wire-subsystems.cabal | 15 + services/brig/src/Brig/API/Client.hs | 2 +- services/brig/src/Brig/API/Internal.hs | 2 +- services/brig/src/Brig/AWS.hs | 2 +- .../brig/src/Brig/CanonicalInterpreter.hs | 2 +- services/brig/src/Brig/Data/Client.hs | 1 - services/brig/src/Brig/Provider/DB.hs | 3 +- services/brig/src/Brig/Team/API.hs | 2 +- services/brig/src/Brig/User/EJPD.hs | 2 +- services/galley/default.nix | 12 - services/galley/galley.cabal | 17 - services/galley/src/Galley/API/Action.hs | 41 +- services/galley/src/Galley/API/Create.hs | 47 +- services/galley/src/Galley/API/Federation.hs | 23 +- services/galley/src/Galley/API/Internal.hs | 22 +- services/galley/src/Galley/API/LegalHold.hs | 72 ++-- .../src/Galley/API/LegalHold/Conflicts.hs | 33 +- .../galley/src/Galley/API/LegalHold/Get.hs | 11 +- .../galley/src/Galley/API/LegalHold/Team.hs | 35 +- .../Galley/API/MLS/Commit/InternalCommit.hs | 8 +- services/galley/src/Galley/API/MLS/Message.hs | 12 +- services/galley/src/Galley/API/MLS/Reset.hs | 5 +- .../src/Galley/API/MLS/SubConversation.hs | 21 +- services/galley/src/Galley/API/Message.hs | 26 +- services/galley/src/Galley/API/Public/Bot.hs | 4 +- services/galley/src/Galley/API/Query.hs | 39 +- services/galley/src/Galley/API/Teams.hs | 270 +++++------- .../galley/src/Galley/API/Teams/Export.hs | 13 +- .../galley/src/Galley/API/Teams/Features.hs | 41 +- .../src/Galley/API/Teams/Features/Get.hs | 37 +- services/galley/src/Galley/API/Update.hs | 154 +++---- services/galley/src/Galley/API/Util.hs | 81 ++-- services/galley/src/Galley/App.hs | 44 +- services/galley/src/Galley/Aws.hs | 194 --------- .../galley/src/Galley/Cassandra/LegalHold.hs | 197 --------- .../galley/src/Galley/Cassandra/Queries.hs | 193 --------- services/galley/src/Galley/Cassandra/Team.hs | 405 +----------------- services/galley/src/Galley/Effects.hs | 26 +- services/galley/src/Galley/Env.hs | 6 +- .../src/Galley/External/LegalHoldService.hs | 4 +- services/galley/src/Galley/Intra/Effects.hs | 43 -- services/galley/src/Galley/Intra/Spar.hs | 48 --- services/galley/src/Galley/Run.hs | 2 +- services/galley/src/Galley/TeamSubsystem.hs | 46 -- services/galley/test/integration/API/SQS.hs | 6 +- .../test/integration/API/Teams/LegalHold.hs | 3 +- .../API/Teams/LegalHold/DisabledByDefault.hs | 3 +- .../integration/API/Teams/LegalHold/Util.hs | 1 - services/galley/test/integration/Run.hs | 2 +- services/galley/test/integration/TestSetup.hs | 2 +- services/spar/src/Spar/Scim/Types.hs | 2 +- services/wire-server-enterprise | 2 +- 90 files changed, 2113 insertions(+), 2014 deletions(-) delete mode 100644 libs/brig-types/src/Brig/Types/Instances.hs rename {services/galley/src/Galley/Effects => libs/wire-subsystems/src/Wire}/LegalHoldStore.hs (51%) create mode 100644 libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs create mode 100644 libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra/Queries.hs create mode 100644 libs/wire-subsystems/src/Wire/LegalHoldStore/Env.hs rename services/galley/src/Galley/Intra/Journal.hs => libs/wire-subsystems/src/Wire/TeamJournal.hs (81%) rename services/galley/src/Galley/Effects/SparAccess.hs => libs/wire-subsystems/src/Wire/TeamJournal/Aws.hs (61%) rename {services/galley/src/Galley/Effects => libs/wire-subsystems/src/Wire}/TeamStore.hs (68%) create mode 100644 libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs create mode 100644 libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs create mode 100644 libs/wire-subsystems/src/Wire/TeamSubsystem/Interpreter.hs delete mode 100644 services/galley/src/Galley/Aws.hs delete mode 100644 services/galley/src/Galley/Cassandra/LegalHold.hs delete mode 100644 services/galley/src/Galley/Intra/Effects.hs delete mode 100644 services/galley/src/Galley/Intra/Spar.hs delete mode 100644 services/galley/src/Galley/TeamSubsystem.hs diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index 817720e0b65..2ce9b7dc42f 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -14,7 +14,6 @@ library exposed-modules: Brig.Types.Activation Brig.Types.Connection - Brig.Types.Instances Brig.Types.Intra Brig.Types.Provider.Tag Brig.Types.Team @@ -72,13 +71,12 @@ library -funbox-strict-fields -Wredundant-constraints -Wunused-packages build-depends: - base >=4 && <5 - , bytestring-conversion >=0.2 + base >=4 && <5 , cassandra-util - , containers >=0.5 + , containers >=0.5 , imports - , QuickCheck >=2.9 - , types-common >=0.16 + , QuickCheck >=2.9 + , types-common >=0.16 , wire-api default-language: GHC2021 diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index d427109a406..4611943535f 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -5,7 +5,6 @@ { mkDerivation , aeson , base -, bytestring-conversion , cassandra-util , containers , gitignoreSource @@ -24,7 +23,6 @@ mkDerivation { src = gitignoreSource ./.; libraryHaskellDepends = [ base - bytestring-conversion cassandra-util containers imports diff --git a/libs/brig-types/src/Brig/Types/Instances.hs b/libs/brig-types/src/Brig/Types/Instances.hs deleted file mode 100644 index ca5fb8f6aa0..00000000000 --- a/libs/brig-types/src/Brig/Types/Instances.hs +++ /dev/null @@ -1,90 +0,0 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} - --- 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 Brig.Types.Instances () where - -import Brig.Types.Provider.Tag -import Cassandra.CQL -import Data.ByteString.Conversion -import Imports -import Wire.API.Provider -import Wire.API.Provider.Service -import Wire.API.User.Client.Prekey - -instance Cql PrekeyId where - ctype = Tagged IntColumn - toCql = CqlInt . fromIntegral . keyId - fromCql (CqlInt i) = pure $ PrekeyId (fromIntegral i) - fromCql _ = Left "PrekeyId: Int expected" - -instance Cql ServiceTag where - ctype = Tagged BigIntColumn - - fromCql (CqlBigInt i) = case intToTag i of - Just t -> pure t - Nothing -> Left $ "unexpected service tag: " ++ show i - fromCql _ = Left "service tag: int expected" - - toCql = CqlBigInt . tagToInt - -instance Cql ServiceKeyPEM where - ctype = Tagged BlobColumn - - fromCql (CqlBlob b) = - maybe - (Left "service key pem: malformed key") - pure - (fromByteString' b) - fromCql _ = Left "service key pem: blob expected" - - toCql = CqlBlob . toByteString - -instance Cql ServiceKey where - ctype = - Tagged - ( UdtColumn - "pubkey" - [ ("typ", IntColumn), - ("size", IntColumn), - ("pem", BlobColumn) - ] - ) - - fromCql (CqlUdt fs) = do - t <- required "typ" - s <- required "size" - p <- required "pem" - case (t :: Int32) of - 0 -> pure $! ServiceKey RsaServiceKey s p - _ -> Left $ "Unexpected service key type: " ++ show t - where - required :: (Cql r) => Text -> Either String r - required f = - maybe - (Left ("ServiceKey: Missing required field '" ++ show f ++ "'")) - fromCql - (lookup f fs) - fromCql _ = Left "service key: udt expected" - - toCql (ServiceKey RsaServiceKey siz pem) = - CqlUdt - [ ("typ", CqlInt 0), - ("size", toCql siz), - ("pem", toCql pem) - ] diff --git a/libs/brig-types/src/Brig/Types/Provider/Tag.hs b/libs/brig-types/src/Brig/Types/Provider/Tag.hs index 7104ba2713b..2aa2704e352 100644 --- a/libs/brig-types/src/Brig/Types/Provider/Tag.hs +++ b/libs/brig-types/src/Brig/Types/Provider/Tag.hs @@ -38,7 +38,7 @@ import Data.Bits import Data.Range import Data.Set qualified as Set import Imports -import Wire.API.Provider.Service.Tag +import Wire.API.Provider.Service.Tag (ServiceTag (..)) newtype Bucket = Bucket Int32 deriving newtype (Cql, Show) diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 2d97f3f5430..eb920a261af 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -1,9 +1,5 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneKindSignatures #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. @@ -24,9 +20,7 @@ -- with this program. If not, see . module Galley.Types.Teams - ( TeamCreationTime (..), - tcTime, - GetFeatureDefaults (..), + ( GetFeatureDefaults (..), FeatureDefaults (..), FeatureFlags, featureDefaults, @@ -38,7 +32,7 @@ module Galley.Types.Teams ) where -import Control.Lens (makeLenses, view) +import Control.Lens (view) import Data.Aeson import Data.Aeson.Key qualified as Key import Data.Aeson.Types qualified as A @@ -53,11 +47,6 @@ import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Permission --- This is the cassandra timestamp of writetime(binding) -newtype TeamCreationTime = TeamCreationTime - { _tcTime :: Int64 - } - -- | Used to extract the feature config type out of 'FeatureDefaults' or -- related types. type family ConfigOf a @@ -399,8 +388,6 @@ instance (FromJSON a) => FromJSON (Defaults a) where parseJSON = withObject "default object" $ \ob -> Defaults <$> (ob .: "defaults") -makeLenses ''TeamCreationTime - notTeamMember :: [UserId] -> [TeamMember] -> [UserId] notTeamMember uids tmms = Set.toList $ diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 9008dcf742e..979ae525e5c 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -51,7 +51,7 @@ module Wire.API.Provider.Service ) where -import Cassandra.CQL qualified as Cql +import Cassandra.CQL hiding (Set) import Control.Lens (makeLenses, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A @@ -124,6 +124,40 @@ instance ToSchema ServiceKey where <*> serviceKeySize .= field "size" schema <*> serviceKeyPEM .= field "pem" schema +instance Cql ServiceKey where + ctype = + Tagged + ( UdtColumn + "pubkey" + [ ("typ", IntColumn), + ("size", IntColumn), + ("pem", BlobColumn) + ] + ) + + fromCql (CqlUdt fs) = do + t <- required "typ" + s <- required "size" + p <- required "pem" + case (t :: Int32) of + 0 -> pure $! ServiceKey RsaServiceKey s p + _ -> Left $ "Unexpected service key type: " ++ show t + where + required :: (Cql r) => Text -> Either String r + required f = + maybe + (Left ("ServiceKey: Missing required field '" ++ show f ++ "'")) + fromCql + (lookup f fs) + fromCql _ = Left "service key: udt expected" + + toCql (ServiceKey RsaServiceKey siz pem) = + CqlUdt + [ ("typ", CqlInt 0), + ("size", toCql siz), + ("pem", toCql pem) + ] + -- | Other types may be supported in the future. data ServiceKeyType = RsaServiceKey @@ -189,6 +223,18 @@ instance Arbitrary ServiceKeyPEM where "-----END PUBLIC KEY-----" ] +instance Cql ServiceKeyPEM where + ctype = Tagged BlobColumn + + fromCql (CqlBlob b) = + maybe + (Left "service key pem: malformed key") + pure + (fromByteString' b) + fromCql _ = Left "service key pem: blob expected" + + toCql = CqlBlob . toByteString + -------------------------------------------------------------------------------- -- Service @@ -236,7 +282,7 @@ instance S.ToSchema ServiceToken where tweak = fmap $ S.schema . S.example ?~ tok tok = "sometoken" -deriving instance Cql.Cql ServiceToken +deriving instance Cql ServiceToken -------------------------------------------------------------------------------- -- ServiceProfile diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 95aaaebab1d..b643e7f0052 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -34,18 +34,29 @@ module Wire.API.Provider.Service.Tag matchAll, match1, match, + Bucket (..), + defBucket, + foldTags, + unfoldTags, + unfoldTagsInto, + diffTags, + nonEmptyTags, + tagToInt, + intToTag, ) where +import Cassandra.CQL hiding (Set) import Control.Lens (Prism', prism) import Data.Aeson (FromJSON, ToJSON (toJSON)) import Data.Attoparsec.ByteString (IResult (..), parse) +import Data.Bits import Data.ByteString (toStrict) import Data.ByteString.Builder qualified as BB import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Conversion import Data.OpenApi qualified as S -import Data.Range (Range, fromRange, rangedSchema) +import Data.Range import Data.Range qualified as Range import Data.Schema import Data.Set qualified as Set @@ -189,6 +200,16 @@ instance S.ToParamSchema ServiceTag where S._schemaEnum = Just (toJSON <$> [(minBound :: ServiceTag) ..]) } +instance Cql ServiceTag where + ctype = Tagged BigIntColumn + + fromCql (CqlBigInt i) = case intToTag i of + Just t -> pure t + Nothing -> Left $ "unexpected service tag: " ++ show i + fromCql _ = Left "service tag: int expected" + + toCql = CqlBigInt . tagToInt + -------------------------------------------------------------------------------- -- Bounded ServiceTag Queries @@ -311,3 +332,109 @@ match1 = matchAll . match match :: ServiceTag -> MatchAll match = MatchAll . Set.singleton + +newtype Bucket = Bucket Int32 + deriving newtype (Cql, Show) + +-- | Bucketing allows us to distribute individual tag bitmasks +-- across multiple wide rows, if it should become necessary. +-- If a tag bitmask it spread across buckets, lookups and deletes +-- on that bitmask will require O(n) queries, where /n/ is the number +-- of buckets for a specific tag, whereas writes can stay at O(1) +-- by always writing to the "newest" bucket. +defBucket :: Bucket +defBucket = Bucket 201608 + +foldTags :: Range 1 3 (Set ServiceTag) -> Int64 +foldTags = foldl' (.|.) 0 . map tagToInt . Set.toList . fromRange + +unfoldTags :: Range 0 3 (Set ServiceTag) -> [Int64] +unfoldTags s = case map tagToInt (Set.toList (fromRange s)) of + [] -> [] + [t] -> [t] + ts@[t, u] -> (t .|. u) : ts + ts@[t, u, v] -> (t .|. u) : (t .|. v) : (u .|. v) : (t .|. u .|. v) : ts + _ -> error "Brig.Provider.DB.Tag: unfoldTags: Too many tags." + +unfoldTagsInto :: Range 1 3 (Set ServiceTag) -> [Int64] -> [Int64] +unfoldTagsInto xs ys = + let xs' = unfoldTags (rcast xs) + in xs' ++ concatMap (\x -> map (.|. x) ys) xs' + +diffTags :: + Range 0 3 (Set ServiceTag) -> + Range 0 3 (Set ServiceTag) -> + Range 0 3 (Set ServiceTag) +diffTags a b = unsafeRange $ Set.difference (fromRange a) (fromRange b) + +nonEmptyTags :: Range m 3 (Set ServiceTag) -> Maybe (Range 1 3 (Set ServiceTag)) +nonEmptyTags r + | Set.null (fromRange r) = Nothing + | otherwise = Just (unsafeRange (fromRange r)) + +tagToInt :: ServiceTag -> Int64 +tagToInt AudioTag = 0b1 +tagToInt BooksTag = 0b10 +tagToInt BusinessTag = 0b100 +tagToInt DesignTag = 0b1000 +tagToInt EducationTag = 0b10000 +tagToInt EntertainmentTag = 0b100000 +tagToInt FinanceTag = 0b1000000 +tagToInt FitnessTag = 0b10000000 +tagToInt FoodDrinkTag = 0b100000000 +tagToInt GamesTag = 0b1000000000 +tagToInt GraphicsTag = 0b10000000000 +tagToInt HealthTag = 0b100000000000 +tagToInt IntegrationTag = 0b1000000000000 +tagToInt LifestyleTag = 0b10000000000000 +tagToInt MediaTag = 0b100000000000000 +tagToInt MedicalTag = 0b1000000000000000 +tagToInt MoviesTag = 0b10000000000000000 +tagToInt MusicTag = 0b100000000000000000 +tagToInt NewsTag = 0b1000000000000000000 +tagToInt PhotographyTag = 0b10000000000000000000 +tagToInt PollTag = 0b100000000000000000000 +tagToInt ProductivityTag = 0b1000000000000000000000 +tagToInt QuizTag = 0b10000000000000000000000 +tagToInt RatingTag = 0b100000000000000000000000 +tagToInt ShoppingTag = 0b1000000000000000000000000 +tagToInt SocialTag = 0b10000000000000000000000000 +tagToInt SportsTag = 0b100000000000000000000000000 +tagToInt TravelTag = 0b1000000000000000000000000000 +tagToInt TutorialTag = 0b10000000000000000000000000000 +tagToInt VideoTag = 0b100000000000000000000000000000 +tagToInt WeatherTag = 0b1000000000000000000000000000000 + +intToTag :: Int64 -> Maybe ServiceTag +intToTag 0b1 = pure AudioTag +intToTag 0b10 = pure BooksTag +intToTag 0b100 = pure BusinessTag +intToTag 0b1000 = pure DesignTag +intToTag 0b10000 = pure EducationTag +intToTag 0b100000 = pure EntertainmentTag +intToTag 0b1000000 = pure FinanceTag +intToTag 0b10000000 = pure FitnessTag +intToTag 0b100000000 = pure FoodDrinkTag +intToTag 0b1000000000 = pure GamesTag +intToTag 0b10000000000 = pure GraphicsTag +intToTag 0b100000000000 = pure HealthTag +intToTag 0b1000000000000 = pure IntegrationTag +intToTag 0b10000000000000 = pure LifestyleTag +intToTag 0b100000000000000 = pure MediaTag +intToTag 0b1000000000000000 = pure MedicalTag +intToTag 0b10000000000000000 = pure MoviesTag +intToTag 0b100000000000000000 = pure MusicTag +intToTag 0b1000000000000000000 = pure NewsTag +intToTag 0b10000000000000000000 = pure PhotographyTag +intToTag 0b100000000000000000000 = pure PollTag +intToTag 0b1000000000000000000000 = pure ProductivityTag +intToTag 0b10000000000000000000000 = pure QuizTag +intToTag 0b100000000000000000000000 = pure RatingTag +intToTag 0b1000000000000000000000000 = pure ShoppingTag +intToTag 0b10000000000000000000000000 = pure SocialTag +intToTag 0b100000000000000000000000000 = pure SportsTag +intToTag 0b1000000000000000000000000000 = pure TravelTag +intToTag 0b10000000000000000000000000000 = pure TutorialTag +intToTag 0b100000000000000000000000000000 = pure VideoTag +intToTag 0b1000000000000000000000000000000 = pure WeatherTag +intToTag _ = Nothing diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index fbc2fd06e23..6dedd4ccdd6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -264,6 +264,12 @@ type ITeamsAPIBase = :> ReqBody '[JSON] UserIds :> Get '[JSON] TeamMemberInfoList ) + :<|> Named + "unchecked-select-team-members" + ( "get-by-ids" + :> ReqBody '[JSON] UserIds + :> Post '[JSON] [TeamMember] + ) :<|> Named "unchecked-get-team-member" ( Capture "uid" UserId @@ -304,6 +310,13 @@ type ITeamsAPIBase = :> CanThrow 'NotATeamMember :> MultiVerb1 'GET '[JSON] (RespondEmpty 200 "User is team owner") ) + :<|> Named + "finalize-delete-team" + ( "finalize-delete" + :> ZLocalUser + :> ZOptConn + :> PostNoContent + ) :<|> "search-visibility" :> ( Named "get-search-visibility-internal" (Get '[JSON] TeamSearchVisibilityView) :<|> Named diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index 283dbaff55b..61be43b4b0c 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -58,6 +58,10 @@ module Wire.API.Team newTeamDeleteData, tdAuthPassword, tdVerificationCode, + + -- * Misc + TeamCreationTime (..), + tcTime, ) where @@ -220,6 +224,13 @@ instance ToSchema Icon where desc = "S3 asset key for an icon image with retention information. Allows special value 'default'." +-- | Cassandra writetime(binding) timestamp +newtype TeamCreationTime = TeamCreationTime + { _tcTime :: Int64 + } + +makeLenses ''TeamCreationTime + data TeamUpdateData = TeamUpdateData { _nameUpdate :: Maybe (Range 1 256 Text), _iconUpdate :: Maybe Icon, diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 73a0c5c0dd4..d11f69e14d0 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -70,6 +70,7 @@ module Wire.API.Team.Member rolePermissions, IsPerm (..), HiddenPerm (..), + mkSingleTeamMembersPage, ) where @@ -237,6 +238,15 @@ newtype TeamMembersPage = TeamMembersPage {unTeamMembersPage :: TeamMembersPage' deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema TeamMembersPage) +mkSingleTeamMembersPage :: [TeamMemberOptPerms] -> TeamMembersPage +mkSingleTeamMembersPage members = + TeamMembersPage + MultiTablePage + { mtpResults = members, + mtpHasMore = False, + mtpPagingState = MultiTablePagingState TeamMembersTable Nothing + } + instance ToSchema TeamMembersPage where schema = object "TeamMembersPage" $ diff --git a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs index 1a8c1edf9b5..e2f8eb04087 100644 --- a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs +++ b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs @@ -32,6 +32,7 @@ module Wire.API.User.Client.Prekey ) where +import Cassandra (ColumnType (IntColumn), Cql (ctype, fromCql, toCql), Tagged (..), Value (CqlInt)) import Crypto.Hash (SHA256, hash) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bits @@ -48,6 +49,12 @@ newtype PrekeyId = PrekeyId {keyId :: Word16} deriving stock (Eq, Ord, Show, Generic) deriving newtype (ToJSON, FromJSON, Arbitrary, S.ToSchema, ToSchema) +instance Cql PrekeyId where + ctype = Tagged IntColumn + toCql = CqlInt . fromIntegral . keyId + fromCql (CqlInt i) = pure $ PrekeyId (fromIntegral i) + fromCql _ = Left "PrekeyId: Int expected" + -------------------------------------------------------------------------------- -- Prekey diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index 3bb1a6658b0..65e5959917d 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -8,6 +8,7 @@ , amazonka , amazonka-core , amazonka-ses +, amazonka-sqs , amqp , async , attoparsec @@ -35,6 +36,7 @@ , extended , extra , file-embed +, galley-types , gitignoreSource , hashable , HaskellNet @@ -75,6 +77,7 @@ , postgresql-error-codes , profunctors , prometheus-client +, proto-lens , QuickCheck , quickcheck-instances , random @@ -101,9 +104,11 @@ , time-out , time-units , tinylog +, tls , token-bucket , transformers , types-common +, types-common-journal , unliftio , unordered-containers , uri-bytestring @@ -128,6 +133,7 @@ mkDerivation { amazonka amazonka-core amazonka-ses + amazonka-sqs amqp async attoparsec @@ -155,6 +161,7 @@ mkDerivation { extended extra file-embed + galley-types hashable HaskellNet HaskellNet-SSL @@ -192,6 +199,7 @@ mkDerivation { postgresql-error-codes profunctors prometheus-client + proto-lens QuickCheck raw-strings-qq resource-pool @@ -214,9 +222,11 @@ mkDerivation { time-out time-units tinylog + tls token-bucket transformers types-common + types-common-journal unliftio unordered-containers uri-bytestring @@ -237,6 +247,7 @@ mkDerivation { amazonka amazonka-core amazonka-ses + amazonka-sqs amqp async attoparsec @@ -263,6 +274,7 @@ mkDerivation { extended extra file-embed + galley-types hashable HaskellNet HaskellNet-SSL @@ -298,6 +310,7 @@ mkDerivation { polysemy-wire-zoo profunctors prometheus-client + proto-lens QuickCheck quickcheck-instances random @@ -327,6 +340,7 @@ mkDerivation { token-bucket transformers types-common + types-common-journal unliftio unordered-containers uri-bytestring diff --git a/libs/wire-subsystems/src/Wire/AWS.hs b/libs/wire-subsystems/src/Wire/AWS.hs index d26bafff38b..45e49680f0d 100644 --- a/libs/wire-subsystems/src/Wire/AWS.hs +++ b/libs/wire-subsystems/src/Wire/AWS.hs @@ -14,22 +14,142 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TemplateHaskell #-} module Wire.AWS where -import Amazonka (Env, runResourceT) -import Amazonka.Core.Lens.Internal qualified as AWS -import Amazonka.Send as AWS -import Amazonka.Types qualified as AWS -import Control.Lens +import Amazonka qualified as AWS +import Amazonka.SQS qualified as SQS +import Amazonka.SQS.Lens qualified as SQS +import Control.Lens hiding ((.=)) +import Control.Monad.Catch +import Control.Monad.Trans.Resource +import Control.Retry (exponentialBackoff, limitRetries, retrying) +import Data.ByteString.Base64 qualified as B64 +import Data.ByteString.Builder (toLazyByteString) +import Data.ProtoLens.Encoding (encodeMessage) +import Data.Text.Encoding (decodeLatin1) +import Data.UUID (toText) +import Data.UUID.V4 (nextRandom) import Imports import Network.HTTP.Client -import Polysemy -import Polysemy.Input + ( HttpException (..), + HttpExceptionContent (..), + Manager, + ) +import Network.TLS qualified as TLS +import Polysemy (Embed, Member, Sem, embed) +import Polysemy.Input (Input, input) +import Proto.TeamEvents qualified as E +import System.Logger qualified as Logger +import System.Logger.Class (Logger, MonadLogger (..)) +import Util.Options (AWSEndpoint (..), awsHost, awsPort, awsSecure) +newtype QueueUrl = QueueUrl Text + deriving (Show) + +data Error where + GeneralError :: (Show e, AWS.AsError e) => e -> Error + +deriving instance Show Error + +deriving instance Typeable Error + +instance Exception Error + +data Env = Env + { _awsEnv :: !AWS.Env, + _logger :: !Logger, + _eventQueue :: !QueueUrl + } + +makeLenses ''Env + +newtype Amazon a = Amazon + { unAmazon :: ReaderT Env (ResourceT IO) a + } + deriving + ( Functor, + Applicative, + Monad, + MonadIO, + MonadThrow, + MonadCatch, + MonadMask, + MonadReader Env, + MonadResource, + MonadUnliftIO + ) + +instance MonadLogger Amazon where + log l m = view logger >>= \g -> Logger.log g l m + +mkEnv :: Logger -> Manager -> AWSEndpoint -> Text -> IO Env +mkEnv lgr mgr endpoint qname = do + let g = Logger.clone (Just "aws") lgr + e <- mkAwsEnv g + q <- getQueueUrl e qname + pure (Env e g (QueueUrl q)) + where + sqs e = AWS.setEndpoint (e ^. awsSecure) (e ^. awsHost) (e ^. awsPort) SQS.defaultService + mkAwsEnv g = do + baseEnv <- + AWS.newEnv AWS.discover + <&> AWS.configureService (sqs endpoint) + pure $ + baseEnv + { AWS.logger = awsLogger g, + AWS.retryCheck = retryCheck, + AWS.manager = mgr + } + awsLogger g l = Logger.log g (mapLevel l) . Logger.msg . toLazyByteString + mapLevel AWS.Info = Logger.Info + mapLevel AWS.Debug = Logger.Trace + mapLevel AWS.Trace = Logger.Trace + mapLevel AWS.Error = Logger.Debug + retryCheck _ InvalidUrlException {} = False + retryCheck n (HttpExceptionRequest _ ex) = case ex of + _ | n >= 3 -> False + NoResponseDataReceived -> True + ConnectionTimeout -> True + ConnectionClosed -> True + ConnectionFailure _ -> True + InternalException x -> case fromException x of + Just TLS.HandshakeFailed {} -> True + _ -> False + _ -> False + getQueueUrl :: AWS.Env -> Text -> IO Text + getQueueUrl e q = do + x <- + runResourceT $ + AWS.trying AWS._Error $ + AWS.send e (SQS.newGetQueueUrl q) + either + (throwM . GeneralError) + (pure . view SQS.getQueueUrlResponse_queueUrl) + x + +execute :: (MonadIO m) => Env -> Amazon a -> m a +execute e m = liftIO $ runResourceT (runReaderT (unAmazon m) e) + +enqueue :: E.TeamEvent -> Amazon () +enqueue ev = do + QueueUrl url <- view eventQueue + dedup <- liftIO nextRandom + amaznkaEnv <- view awsEnv + let body = decodeLatin1 $ B64.encode $ encodeMessage ev + req = + SQS.newSendMessage url body + & SQS.sendMessage_messageGroupId ?~ "team.events" + & SQS.sendMessage_messageDeduplicationId ?~ toText dedup + res <- retrying (limitRetries 5 <> exponentialBackoff 1000000) (const (pure . canRetry)) $ const (sendCatchEnv amaznkaEnv req) + either (throwM . GeneralError) (const (pure ())) res + +-- Polysemy-style helper used by existing code sendCatch :: - ( Member (Input Amazonka.Env) r, - Member (Embed IO) r, + ( Member (Embed IO) r, + Member (Input AWS.Env) r, AWS.AWSRequest req, Typeable req, Typeable (AWS.AWSResponse req) @@ -38,7 +158,18 @@ sendCatch :: Sem r (Either AWS.Error (AWS.AWSResponse req)) sendCatch req = do env <- input - embed . AWS.trying AWS._Error . runResourceT . AWS.send env $ req + embed $ runResourceT (AWS.trying AWS._Error (AWS.send env req)) + +-- Amazon monad variant +sendCatchEnv :: + ( AWS.AWSRequest r, + Typeable r, + Typeable (AWS.AWSResponse r) + ) => + AWS.Env -> + r -> + Amazon (Either AWS.Error (AWS.AWSResponse r)) +sendCatchEnv e = AWS.trying AWS._Error . AWS.send e canRetry :: Either AWS.Error a -> Bool canRetry (Right _) = False diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs index e9055a1f399..7353ad6092f 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs @@ -143,14 +143,6 @@ runSyncUserGroupAndChannel (SyncUserGroupAndChannel {..}) = do action def -localBotsAndUsers :: (Foldable f) => f LocalMember -> ([BotMember], [LocalMember]) -localBotsAndUsers = foldMap botOrUser - where - botOrUser m = case m.service of - -- we drop invalid bots here, which shouldn't happen - Just _ -> (toList (newBotMember m), []) - Nothing -> ([], [m]) - runSyncUserGroup :: ( Member UserGroupStore r, Member BackgroundJobsPublisher r, diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs index 2cc7001a013..0cfd6d8bc82 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs @@ -33,7 +33,6 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API (makeConversationUpdateBundle, sendBundle) import Wire.API.Federation.API.Galley.Notifications (ConversationUpdate (..)) import Wire.API.Federation.Error (FederationError) -import Wire.API.Push.V2 qualified as PushV2 import Wire.BackendNotificationQueueAccess (BackendNotificationQueueAccess, enqueueNotificationsConcurrently) import Wire.ConversationSubsystem import Wire.ExternalAccess (ExternalAccess, deliverAsync) @@ -130,6 +129,3 @@ pushConversationEvent conn st e lusers bots = do recipients = map userRecipient (tUnqualified users), isCellsEvent = shouldPushToCells st e } - - userRecipient :: UserId -> Recipient - userRecipient u = Recipient {recipientUserId = u, recipientClients = PushV2.RecipientClientsAll} diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index fb2544f7066..52b53465d5a 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -1,19 +1,3 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2023 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 . {-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. @@ -99,7 +83,7 @@ data GalleyAPIAccess m a where UserId -> TeamId -> GalleyAPIAccess m (Maybe Team.TeamMember) - GetTeamMembers :: + GetTeamMembersWithLimit :: TeamId -> Maybe (Range 1 Team.HardTruncationLimit Int32) -> GalleyAPIAccess m Team.TeamMemberList @@ -107,6 +91,10 @@ data GalleyAPIAccess m a where TeamId -> [UserId] -> GalleyAPIAccess m Team.TeamMemberInfoList + SelectTeamMembers :: + TeamId -> + [UserId] -> + GalleyAPIAccess m [Team.TeamMember] GetTeamId :: UserId -> GalleyAPIAccess m (Maybe TeamId) @@ -129,6 +117,11 @@ data GalleyAPIAccess m a where Team.TeamStatus -> Maybe Currency.Alpha -> GalleyAPIAccess m () + FinalizeDeleteTeam :: + Local UserId -> + Maybe ConnId -> + TeamId -> + GalleyAPIAccess m () MemberIsTeamOwner :: TeamId -> UserId -> diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index bcce4a24488..86b070bd836 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -78,8 +78,9 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = AddTeamMember id' id'' a b -> addTeamMember id' id'' a b CreateTeam id' bnt id'' -> createTeam id' bnt id'' GetTeamMember id' id'' -> getTeamMember id' id'' - GetTeamMembers tid maxResults -> getTeamMembers tid maxResults + GetTeamMembersWithLimit tid maxResults -> getTeamMembersWithLimit tid maxResults SelectTeamMemberInfos tid uids -> selectTeamMemberInfos tid uids + SelectTeamMembers tid uids -> selectTeamMembers tid uids GetTeamId id' -> getTeamId id' GetTeam id' -> getTeam id' GetTeamName id' -> getTeamName id' @@ -88,6 +89,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = GetFeatureConfigForTeam tid -> getFeatureConfigForTeam tid GetUserLegalholdStatus id' tid -> getUserLegalholdStatus id' tid ChangeTeamStatus id' ts m_al -> changeTeamStatus id' ts m_al + FinalizeDeleteTeam lusr mconn tid -> finalizeDeleteTeam lusr mconn tid MemberIsTeamOwner id' id'' -> memberIsTeamOwner id' id'' GetAllTeamFeaturesForUser m_id' -> getAllTeamFeaturesForUser m_id' GetVerificationCodeEnabled id' -> getVerificationCodeEnabled id' @@ -338,7 +340,7 @@ getTeamMember u tid = do -- | TODO: is now truncated. this is (only) used for team suspension / unsuspension, which -- means that only the first 2000 members of a team (according to some arbitrary order) will -- be suspended, and the rest will remain active. -getTeamMembers :: +getTeamMembersWithLimit :: ( Member (Error ParseException) r, Member Rpc r, Member (Input Endpoint) r, @@ -347,7 +349,7 @@ getTeamMembers :: TeamId -> Maybe (Range 1 HardTruncationLimit Int32) -> Sem r TeamMemberList -getTeamMembers tid maxResults = do +getTeamMembersWithLimit tid maxResults = do debug $ remote "galley" . msg (val "Get team members") galleyRequest req >>= decodeBodyOrThrow "galley" where @@ -378,6 +380,25 @@ selectTeamMemberInfos tid uids = do . lbytes (encode bdy) . expect2xx +selectTeamMembers :: + ( Member (Error ParseException) r, + Member Rpc r, + Member (Input Endpoint) r + ) => + TeamId -> + [UserId] -> + Sem r [TeamMember] +selectTeamMembers tid uids = do + let bdy = UserIds uids + galleyRequest (req bdy) >>= decodeBodyOrThrow "galley" + where + req bdy = + method POST + . paths ["i", "teams", toByteString' tid, "members", "get-by-ids"] + . header "Content-Type" "application/json" + . lbytes (encode bdy) + . expect2xx + getTeamAdmins :: ( Member (Error ParseException) r, Member Rpc r, @@ -577,6 +598,24 @@ changeTeamStatus tid s cur = do . expect2xx . lbytes (encode $ Team.TeamStatusUpdate s cur) +finalizeDeleteTeam :: + ( Member Rpc r, + Member (Input Endpoint) r + ) => + Local UserId -> + Maybe ConnId -> + TeamId -> + Sem r () +finalizeDeleteTeam lusr mconn tid = do + void $ galleyRequest req + where + req = + method POST + . paths ["i", "teams", toByteString' tid, "finalize-delete"] + . zUser (tUnqualified lusr) + . maybe id (header "Z-Connection" . fromConnId) mconn + . expect2xx + getTeamExposeInvitationURLsToTeamAdmin :: ( Member Rpc r, Member (Input Endpoint) r, diff --git a/services/galley/src/Galley/Effects/LegalHoldStore.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore.hs similarity index 51% rename from services/galley/src/Galley/Effects/LegalHoldStore.hs rename to libs/wire-subsystems/src/Wire/LegalHoldStore.hs index 0cea5e4298b..66f675cb49c 100644 --- a/services/galley/src/Galley/Effects/LegalHoldStore.hs +++ b/libs/wire-subsystems/src/Wire/LegalHoldStore.hs @@ -1,46 +1,7 @@ {-# LANGUAGE TemplateHaskell #-} --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . +module Wire.LegalHoldStore where -module Galley.Effects.LegalHoldStore - ( -- * LegalHold store effect - LegalHoldStore (..), - - -- * Store actions - createSettings, - getSettings, - removeSettings, - insertPendingPrekeys, - selectPendingPrekeys, - dropPendingPrekeys, - setUserLegalHoldStatus, - setTeamLegalholdWhitelisted, - unsetTeamLegalholdWhitelisted, - isTeamLegalholdWhitelisted, - validateServiceKey, - - -- * Intra actions - makeVerifiedRequest, - makeVerifiedRequestFreshManager, - ) -where - -import Brig.Types.Team.LegalHold import Data.ByteString.Lazy.Char8 qualified as LC8 import Data.Id import Data.LegalHold @@ -49,6 +10,7 @@ import Imports import Network.HTTP.Client qualified as Http import Polysemy import Wire.API.Provider.Service +import Wire.API.Team.LegalHold.Internal import Wire.API.User.Client.Prekey data LegalHoldStore m a where @@ -62,7 +24,6 @@ data LegalHoldStore m a where SetTeamLegalholdWhitelisted :: TeamId -> LegalHoldStore m () UnsetTeamLegalholdWhitelisted :: TeamId -> LegalHoldStore m () IsTeamLegalholdWhitelisted :: TeamId -> LegalHoldStore m Bool - -- intra actions MakeVerifiedRequestFreshManager :: Fingerprint Rsa -> HttpsUrl -> diff --git a/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs new file mode 100644 index 00000000000..e0d473b5b79 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs @@ -0,0 +1,148 @@ +module Wire.LegalHoldStore.Cassandra (interpretLegalHoldStoreToCassandra, validateServiceKey) where + +import Cassandra +import Control.Exception (catch) +import Data.ByteString.Conversion.To +import Data.ByteString.Lazy.Char8 qualified as LC8 +import Data.Id +import Data.LegalHold +import Data.Misc +import Galley.Types.Teams (FeatureDefaults (..)) +import Imports +import OpenSSL.EVP.Digest qualified as SSL +import OpenSSL.EVP.PKey qualified as SSL +import OpenSSL.PEM qualified as SSL +import OpenSSL.RSA qualified as SSL +import Polysemy +import Polysemy.Input +import Polysemy.TinyLog +import Ssl.Util qualified as SSL +import Wire.API.Provider.Service +import Wire.API.Team.Feature (LegalholdConfig) +import Wire.API.Team.LegalHold.Internal +import Wire.API.User.Client.Prekey +import Wire.LegalHoldStore (LegalHoldStore (..)) +import Wire.LegalHoldStore.Cassandra.Queries qualified as Q +import Wire.LegalHoldStore.Env (LegalHoldEnv (..)) +import Wire.TeamStore.Cassandra.Queries qualified as QTS +import Wire.Util (embedClientInput, logEffect) + +interpretLegalHoldStoreToCassandra :: + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member (Input LegalHoldEnv) r, + Member TinyLog r + ) => + FeatureDefaults LegalholdConfig -> + Sem (LegalHoldStore ': r) a -> + Sem r a +interpretLegalHoldStoreToCassandra lh = interpret $ \case + CreateSettings s -> do + logEffect "LegalHoldStore.CreateSettings" + embedClientInput $ createSettings s + GetSettings tid -> do + logEffect "LegalHoldStore.GetSettings" + embedClientInput $ getSettings tid + RemoveSettings tid -> do + logEffect "LegalHoldStore.RemoveSettings" + embedClientInput $ removeSettings tid + InsertPendingPrekeys uid pkeys -> do + logEffect "LegalHoldStore.InsertPendingPrekeys" + embedClientInput $ insertPendingPrekeys uid pkeys + SelectPendingPrekeys uid -> do + logEffect "LegalHoldStore.SelectPendingPrekeys" + embedClientInput $ selectPendingPrekeys uid + DropPendingPrekeys uid -> do + logEffect "LegalHoldStore.DropPendingPrekeys" + embedClientInput $ dropPendingPrekeys uid + SetUserLegalHoldStatus tid uid st -> do + logEffect "LegalHoldStore.SetUserLegalHoldStatus" + embedClientInput $ setUserLegalHoldStatus tid uid st + SetTeamLegalholdWhitelisted tid -> do + logEffect "LegalHoldStore.SetTeamLegalholdWhitelisted" + embedClientInput $ setTeamLegalholdWhitelisted tid + UnsetTeamLegalholdWhitelisted tid -> do + logEffect "LegalHoldStore.UnsetTeamLegalholdWhitelisted" + embedClientInput $ unsetTeamLegalholdWhitelisted tid + IsTeamLegalholdWhitelisted tid -> do + logEffect "LegalHoldStore.IsTeamLegalholdWhitelisted" + embedClientInput $ isTeamLegalholdWhitelisted lh tid + MakeVerifiedRequestFreshManager fpr url r -> do + logEffect "LegalHoldStore.MakeVerifiedRequestFreshManager" + env <- input + embed @IO $ makeVerifiedRequestFreshManager env fpr url r + MakeVerifiedRequest fpr url r -> do + logEffect "LegalHoldStore.MakeVerifiedRequest" + env <- input + embed @IO $ makeVerifiedRequest env fpr url r + ValidateServiceKey sk -> do + logEffect "LegalHoldStore.ValidateServiceKey" + embed @IO $ validateServiceKey sk + +createSettings :: (MonadClient m) => LegalHoldService -> m () +createSettings (LegalHoldService tid url fpr tok key) = + retry x1 $ write Q.insertLegalHoldSettings (params LocalQuorum (url, fpr, tok, key, tid)) + +getSettings :: (MonadClient m) => TeamId -> m (Maybe LegalHoldService) +getSettings tid = fmap toLegalHoldService <$> retry x1 (query1 Q.selectLegalHoldSettings (params LocalQuorum (Identity tid))) + where + toLegalHoldService (httpsUrl, fingerprint, tok, key) = LegalHoldService tid httpsUrl fingerprint tok key + +removeSettings :: (MonadClient m) => TeamId -> m () +removeSettings tid = retry x5 (write Q.removeLegalHoldSettings (params LocalQuorum (Identity tid))) + +insertPendingPrekeys :: (MonadClient m) => UserId -> [Prekey] -> m () +insertPendingPrekeys uid keys = retry x5 . batch $ do + forM_ keys $ \(Prekey keyId key) -> addPrepQuery Q.insertPendingPrekeys (uid, keyId, key) + +selectPendingPrekeys :: (MonadClient m) => UserId -> m (Maybe ([Prekey], LastPrekey)) +selectPendingPrekeys uid = pickLastKey . fmap fromTuple <$> retry x1 (query Q.selectPendingPrekeys (params LocalQuorum (Identity uid))) + where + fromTuple (keyId, key) = Prekey keyId key + pickLastKey allPrekeys = case unsnoc allPrekeys of + Nothing -> Nothing + Just (keys, lst) -> pure (keys, lastPrekey . prekeyKey $ lst) + +dropPendingPrekeys :: (MonadClient m) => UserId -> m () +dropPendingPrekeys uid = retry x5 (write Q.dropPendingPrekeys (params LocalQuorum (Identity uid))) + +setUserLegalHoldStatus :: (MonadClient m) => TeamId -> UserId -> UserLegalHoldStatus -> m () +setUserLegalHoldStatus tid uid status = retry x5 (write Q.updateUserLegalHoldStatus (params LocalQuorum (status, tid, uid))) + +setTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () +setTeamLegalholdWhitelisted tid = retry x5 (write Q.insertLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) + +unsetTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () +unsetTeamLegalholdWhitelisted tid = retry x5 (write Q.removeLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) + +isTeamLegalholdWhitelisted :: FeatureDefaults LegalholdConfig -> TeamId -> Client Bool +isTeamLegalholdWhitelisted FeatureLegalHoldDisabledPermanently _ = pure False +isTeamLegalholdWhitelisted FeatureLegalHoldDisabledByDefault _ = pure False +isTeamLegalholdWhitelisted FeatureLegalHoldWhitelistTeamsAndImplicitConsent tid = + isJust <$> (runIdentity <$$> retry x5 (query1 QTS.selectLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid)))) + +validateServiceKey :: (MonadIO m) => ServiceKeyPEM -> m (Maybe (ServiceKey, Fingerprint Rsa)) +validateServiceKey pem = + liftIO $ + readPublicKey >>= \pk -> + case SSL.toPublicKey =<< pk of + Nothing -> pure Nothing + Just pk' -> do + Just sha <- SSL.getDigestByName "SHA256" + let size = SSL.rsaSize (pk' :: SSL.RSAPubKey) + if size < minRsaKeySize + then pure Nothing + else do + fpr <- Fingerprint <$> SSL.rsaFingerprint sha pk' + let bits = fromIntegral size * 8 + let key = ServiceKey RsaServiceKey bits pem + pure (Just (key, fpr)) + where + readPublicKey = + ( do + pk <- SSL.readPublicKey (LC8.unpack (toByteString pem)) + pure (Just pk) + ) + `catch` (\(_ :: SomeException) -> pure Nothing) + minRsaKeySize :: Int + minRsaKeySize = 256 diff --git a/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra/Queries.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra/Queries.hs new file mode 100644 index 00000000000..97c072a8903 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra/Queries.hs @@ -0,0 +1,75 @@ +module Wire.LegalHoldStore.Cassandra.Queries where + +import Cassandra as C +import Data.Functor.Identity (Identity) +import Data.Id +import Data.LegalHold +import Data.Misc +import Data.Text (Text) +import Text.RawString.QQ +import Wire.API.Provider.Service +import Wire.API.User.Client.Prekey + +insertLegalHoldSettings :: PrepQuery W (HttpsUrl, Fingerprint Rsa, ServiceToken, ServiceKey, TeamId) () +insertLegalHoldSettings = + [r| + update legalhold_service + set base_url = ?, + fingerprint = ?, + auth_token = ?, + pubkey = ? + where team_id = ? + |] + +selectLegalHoldSettings :: PrepQuery R (Identity TeamId) (HttpsUrl, Fingerprint Rsa, ServiceToken, ServiceKey) +selectLegalHoldSettings = + [r| + select base_url, fingerprint, auth_token, pubkey + from legalhold_service + where team_id = ? + |] + +removeLegalHoldSettings :: PrepQuery W (Identity TeamId) () +removeLegalHoldSettings = "delete from legalhold_service where team_id = ?" + +insertPendingPrekeys :: PrepQuery W (UserId, PrekeyId, Text) () +insertPendingPrekeys = + [r| + insert into legalhold_pending_prekeys (user, key, data) values (?, ?, ?) + |] + +dropPendingPrekeys :: PrepQuery W (Identity UserId) () +dropPendingPrekeys = + [r| + delete from legalhold_pending_prekeys + where user = ? + |] + +selectPendingPrekeys :: PrepQuery R (Identity UserId) (PrekeyId, Text) +selectPendingPrekeys = + [r| + select key, data + from legalhold_pending_prekeys + where user = ? + order by key asc + |] + +updateUserLegalHoldStatus :: PrepQuery W (UserLegalHoldStatus, TeamId, UserId) () +updateUserLegalHoldStatus = + [r| + update team_member + set legalhold_status = ? + where team = ? and user = ? + |] + +insertLegalHoldWhitelistedTeam :: PrepQuery W (Identity TeamId) () +insertLegalHoldWhitelistedTeam = + [r| + insert into legalhold_whitelisted (team) values (?) + |] + +removeLegalHoldWhitelistedTeam :: PrepQuery W (Identity TeamId) () +removeLegalHoldWhitelistedTeam = + [r| + delete from legalhold_whitelisted where team = ? + |] diff --git a/libs/wire-subsystems/src/Wire/LegalHoldStore/Env.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore/Env.hs new file mode 100644 index 00000000000..17926ebce6c --- /dev/null +++ b/libs/wire-subsystems/src/Wire/LegalHoldStore/Env.hs @@ -0,0 +1,11 @@ +module Wire.LegalHoldStore.Env where + +import Data.ByteString.Lazy.Char8 qualified as LC8 +import Data.Misc +import Imports +import Network.HTTP.Client qualified as Http + +data LegalHoldEnv = LegalHoldEnv + { makeVerifiedRequest :: Fingerprint Rsa -> HttpsUrl -> (Http.Request -> Http.Request) -> IO (Http.Response LC8.ByteString), + makeVerifiedRequestFreshManager :: Fingerprint Rsa -> HttpsUrl -> (Http.Request -> Http.Request) -> IO (Http.Response LC8.ByteString) + } diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs index 2ffe5d13b22..750d90885a9 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs @@ -20,6 +20,7 @@ module Wire.NotificationSubsystem where import Control.Concurrent.Async (Async) +import Control.Lens (view) import Data.Aeson import Data.Default import Data.Id @@ -28,7 +29,11 @@ import Polysemy import Wire.API.Event.Conversation import Wire.API.Federation.API.Galley.Notifications (ConversationUpdate) import Wire.API.Push.V2 hiding (Push (..), Recipient, newPush) +import Wire.API.Push.V2 qualified as PushV2 +import Wire.API.Team.Member +import Wire.API.Team.Member qualified as Mem import Wire.Arbitrary +import Wire.StoredConversation (LocalMember (..)) data Recipient = Recipient { recipientUserId :: UserId, @@ -37,6 +42,16 @@ data Recipient = Recipient deriving stock (Show, Ord, Eq, Generic) deriving (Arbitrary) via GenericUniform Recipient +userRecipient :: UserId -> Recipient +userRecipient u = Recipient u PushV2.RecipientClientsAll + +membersToRecipients :: Maybe UserId -> [TeamMember] -> [Recipient] +membersToRecipients Nothing = map (userRecipient . view Mem.userId) +membersToRecipients (Just u) = map userRecipient . filter (/= u) . map (view Mem.userId) + +localMemberToRecipient :: LocalMember -> Recipient +localMemberToRecipient = userRecipient . (.id_) + data Push = Push { conn :: Maybe ConnId, transient :: Bool, diff --git a/libs/wire-subsystems/src/Wire/SparAPIAccess.hs b/libs/wire-subsystems/src/Wire/SparAPIAccess.hs index 7435ac2af55..b2df76bd01a 100644 --- a/libs/wire-subsystems/src/Wire/SparAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/SparAPIAccess.hs @@ -1,19 +1,3 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 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 . {-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. @@ -37,9 +21,12 @@ module Wire.SparAPIAccess where import Data.Id import Polysemy +import Wire.API.User import Wire.API.User.IdentityProvider data SparAPIAccess m a where GetIdentityProviders :: TeamId -> SparAPIAccess m IdPList + DeleteTeam :: TeamId -> SparAPIAccess m () + LookupScimUserInfo :: UserId -> SparAPIAccess m ScimUserInfo makeSem ''SparAPIAccess diff --git a/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs index 5ded03c9398..fc08de1b81d 100644 --- a/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs @@ -30,6 +30,7 @@ import Polysemy.Input import Polysemy.TinyLog import System.Logger.Message import Util.Options +import Wire.API.User (ScimUserInfo) import Wire.API.User.IdentityProvider import Wire.ParseException import Wire.Rpc @@ -47,6 +48,8 @@ interpretSparAPIAccessToRpc sparEndpoint = interpret $ runInputConst sparEndpoint . \case GetIdentityProviders tid -> getIdentityProvidersImpl tid + DeleteTeam tid -> deleteTeamImpl tid + LookupScimUserInfo uid -> lookupScimUserInfoImpl uid sparRequest :: (Member Rpc r, Member (Input Endpoint) r) => @@ -75,6 +78,36 @@ getIdentityProvidersImpl tid = do method GET . paths ["i", "identity-providers", toByteString' tid] +-- | Notify Spar that a team is being deleted. +deleteTeamImpl :: + ( Member (Input Endpoint) r, + Member Rpc r + ) => + TeamId -> + Sem r () +deleteTeamImpl tid = do + void $ sparRequest delReq + where + delReq = + method DELETE + . paths ["i", "teams", toByteString' tid] + . expect2xx + +-- | Get the SCIM user info for a user. +lookupScimUserInfoImpl :: + ( Member (Error ParseException) r, + Member (Input Endpoint) r, + Member Rpc r + ) => + UserId -> + Sem r ScimUserInfo +lookupScimUserInfoImpl uid = do + decodeBodyOrThrow "spar" =<< sparRequest postReq + where + postReq = + method POST + . paths ["i", "scim", "userinfo", toByteString' uid] + -- FUTUREWORK: This is duplicated in Wire/GalleyAPIAccess/Rpc. Move to a common module. decodeBodyOrThrow :: forall a r. (Typeable a, FromJSON a, Member (Error ParseException) r) => Text -> Response (Maybe BL.ByteString) -> Sem r a decodeBodyOrThrow ctx r = either (throw . ParseException ctx) pure (responseJsonEither r) diff --git a/libs/wire-subsystems/src/Wire/StoredConversation.hs b/libs/wire-subsystems/src/Wire/StoredConversation.hs index 85ac05ac2cf..3de3e3a8f46 100644 --- a/libs/wire-subsystems/src/Wire/StoredConversation.hs +++ b/libs/wire-subsystems/src/Wire/StoredConversation.hs @@ -28,6 +28,7 @@ import Data.Qualified import Data.Set qualified as Set import Data.Time (UTCTime) import Data.UUID.Tagged qualified as U +import Galley.Types.Teams (isTeamMember) import Imports import Wire.API.Conversation import Wire.API.Conversation.CellsState @@ -37,6 +38,7 @@ import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group.Serialisation qualified as MLS import Wire.API.MLS.SubConversation import Wire.API.Provider.Service +import Wire.API.Team.Member import Wire.API.User import Wire.UserList @@ -363,3 +365,18 @@ botMemId m = BotId $ m.fromBotMember.id_ botMemService :: BotMember -> ServiceRef botMemService m = fromJust $ m.fromBotMember.service + +localBotsAndUsers :: (Foldable f) => f LocalMember -> ([BotMember], [LocalMember]) +localBotsAndUsers = foldMap botOrUser + where + botOrUser m = case m.service of + -- we drop invalid bots here, which shouldn't happen + Just _ -> (toList (newBotMember m), []) + Nothing -> ([], [m]) + +nonTeamMembers :: [LocalMember] -> [TeamMember] -> [LocalMember] +nonTeamMembers cm tm = filter (not . isMemberOfTeam . (.id_)) cm + where + -- FUTUREWORK: remote members: teams and their members are always on the same backend + isMemberOfTeam = \case + uid -> isTeamMember uid tm diff --git a/services/galley/src/Galley/Intra/Journal.hs b/libs/wire-subsystems/src/Wire/TeamJournal.hs similarity index 81% rename from services/galley/src/Galley/Intra/Journal.hs rename to libs/wire-subsystems/src/Wire/TeamJournal.hs index 51221065ac6..c8e6ba64a00 100644 --- a/services/galley/src/Galley/Intra/Journal.hs +++ b/libs/wire-subsystems/src/Wire/TeamJournal.hs @@ -1,6 +1,8 @@ +{-# LANGUAGE TemplateHaskell #-} + -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2025 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 @@ -15,14 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Intra.Journal - ( teamActivate, - teamUpdate, - teamDelete, - teamSuspend, - evData, - ) -where +module Wire.TeamJournal where import Control.Lens import Data.Currency qualified as Currency @@ -31,15 +26,20 @@ import Data.Proto.Id import Data.ProtoLens (defMessage) import Data.Text (pack) import Data.Time.Clock.POSIX -import Galley.Effects.TeamStore -import Galley.Types.Teams import Imports hiding (head) import Numeric.Natural import Polysemy -import Proto.TeamEvents (TeamEvent'EventData, TeamEvent'EventType (..)) +import Proto.TeamEvents (TeamEvent, TeamEvent'EventData, TeamEvent'EventType (..)) import Proto.TeamEvents_Fields qualified as T +import Wire.API.Team (TeamCreationTime (..)) import Wire.Sem.Now import Wire.Sem.Now qualified as Now +import Wire.TeamStore + +data TeamJournal m a where + EnqueueTeamEvent :: TeamEvent -> TeamJournal m () + +makeSem ''TeamJournal -- Note [journaling] -- ~~~~~~~~~~~~~~~~~ @@ -48,7 +48,8 @@ import Wire.Sem.Now qualified as Now teamActivate :: ( Member Now r, - Member TeamStore r + Member TeamStore r, + Member TeamJournal r ) => TeamId -> Natural -> @@ -60,8 +61,8 @@ teamActivate tid teamSize cur time = do journalEvent TeamEvent'TEAM_ACTIVATE tid (Just $ evData teamSize owners cur) time teamUpdate :: - ( Member TeamStore r, - Member Now r + ( Member Now r, + Member TeamJournal r ) => TeamId -> Natural -> @@ -71,24 +72,24 @@ teamUpdate tid teamSize billingUserIds = journalEvent TeamEvent'TEAM_UPDATE tid (Just $ evData teamSize billingUserIds Nothing) Nothing teamDelete :: - ( Member TeamStore r, - Member Now r + ( Member Now r, + Member TeamJournal r ) => TeamId -> Sem r () teamDelete tid = journalEvent TeamEvent'TEAM_DELETE tid Nothing Nothing teamSuspend :: - ( Member TeamStore r, - Member Now r + ( Member Now r, + Member TeamJournal r ) => TeamId -> Sem r () teamSuspend tid = journalEvent TeamEvent'TEAM_SUSPEND tid Nothing Nothing journalEvent :: - ( Member TeamStore r, - Member Now r + ( Member Now r, + Member TeamJournal r ) => TeamEvent'EventType -> TeamId -> @@ -98,7 +99,7 @@ journalEvent :: journalEvent typ tid dat tim = do -- writetime is in microseconds in cassandra 3.11 now <- round . utcTimeToPOSIXSeconds <$> Now.get - let ts = maybe now ((`div` 1000000) . view tcTime) tim + let ts = maybe now ((`div` 1000000) . _tcTime) tim ev = defMessage & T.eventType .~ typ diff --git a/services/galley/src/Galley/Effects/SparAccess.hs b/libs/wire-subsystems/src/Wire/TeamJournal/Aws.hs similarity index 61% rename from services/galley/src/Galley/Effects/SparAccess.hs rename to libs/wire-subsystems/src/Wire/TeamJournal/Aws.hs index f84e3ac87ec..b9eb3e79911 100644 --- a/services/galley/src/Galley/Effects/SparAccess.hs +++ b/libs/wire-subsystems/src/Wire/TeamJournal/Aws.hs @@ -1,8 +1,6 @@ -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2025 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 @@ -17,14 +15,22 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Effects.SparAccess where +module Wire.TeamJournal.Aws + ( interpretTeamJournal, + ) +where -import Data.Id +import Imports import Polysemy -import Wire.API.User (ScimUserInfo) - -data SparAccess m a where - DeleteTeam :: TeamId -> SparAccess m () - LookupScimUserInfo :: UserId -> SparAccess m ScimUserInfo +import Wire.AWS qualified as WA +import Wire.TeamJournal (TeamJournal (..)) -makeSem ''SparAccess +interpretTeamJournal :: + (Member (Embed IO) r) => + Maybe WA.Env -> + Sem (TeamJournal ': r) a -> + Sem r a +interpretTeamJournal mEnv = interpret $ \case + EnqueueTeamEvent ev -> case mEnv of + Nothing -> pure () + Just e -> embed $ WA.execute e (WA.enqueue ev) diff --git a/services/galley/src/Galley/Effects/TeamStore.hs b/libs/wire-subsystems/src/Wire/TeamStore.hs similarity index 68% rename from services/galley/src/Galley/Effects/TeamStore.hs rename to libs/wire-subsystems/src/Wire/TeamStore.hs index 96300b755ef..bf45dd0cdf5 100644 --- a/services/galley/src/Galley/Effects/TeamStore.hs +++ b/libs/wire-subsystems/src/Wire/TeamStore.hs @@ -2,7 +2,7 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2025 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 @@ -17,82 +17,21 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Effects.TeamStore - ( -- * Team store effect - TeamStore (..), - - -- * Teams - - -- ** Create teams - createTeam, - - -- ** Read teams - getTeam, - getTeamName, - getTeamBinding, - getTeamsBindings, - getTeamCreationTime, - listTeams, - selectTeams, - getUserTeams, - getUsersTeams, - getOneUserTeam, - lookupBindingTeam, - - -- ** Update teams - setTeamData, - setTeamStatus, - - -- ** Delete teams - deleteTeam, - - -- * Team Members - - -- ** Create team members - createTeamMember, - - -- ** Read team members - getTeamMember, - getTeamMembersWithLimit, - getTeamMembers, - getBillingTeamMembers, - getTeamAdmins, - selectTeamMembers, - selectTeamMemberInfos, - selectTeamMembersPaginated, - - -- ** Update team members - setTeamMemberPermissions, - - -- ** Delete team members - deleteTeamMember, - - -- * Configuration - fanoutLimit, - getLegalHoldFlag, - - -- * Events - enqueueTeamEvent, - ) -where +module Wire.TeamStore where import Data.Id import Data.Range -import Galley.Types.Teams import Imports import Polysemy -import Proto.TeamEvents qualified as E import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team -import Wire.API.Team.Feature import Wire.API.Team.Member (HardTruncationLimit, TeamMember, TeamMemberList) import Wire.API.Team.Member.Info (TeamMemberInfo) import Wire.API.Team.Permission import Wire.ListItems import Wire.Sem.Paging -import Wire.Sem.Paging.Cassandra (CassandraPaging) data TeamStore m a where CreateTeamMember :: TeamId -> TeamMember -> TeamStore m () @@ -116,12 +55,6 @@ data TeamStore m a where GetTeamMembers :: TeamId -> TeamStore m [TeamMember] SelectTeamMembers :: TeamId -> [UserId] -> TeamStore m [TeamMember] SelectTeamMemberInfos :: TeamId -> [UserId] -> TeamStore m [TeamMemberInfo] - SelectTeamMembersPaginated :: - TeamId -> - [UserId] -> - Maybe (PagingState CassandraPaging TeamMember) -> - PagingBounds CassandraPaging TeamMember -> - TeamStore m (Page CassandraPaging TeamMember) -- FUTUREWORK(mangoiv): this should be a single 'TeamId' (@'Maybe' 'TeamId'@), there's no way -- a user could be part of multiple teams GetUserTeams :: UserId -> TeamStore m [TeamId] @@ -133,9 +66,6 @@ data TeamStore m a where DeleteTeam :: TeamId -> TeamStore m () SetTeamData :: TeamId -> TeamUpdateData -> TeamStore m () SetTeamStatus :: TeamId -> TeamStatus -> TeamStore m () - FanoutLimit :: TeamStore m (Range 1 HardTruncationLimit Int32) - GetLegalHoldFlag :: TeamStore m (FeatureDefaults LegalholdConfig) - EnqueueTeamEvent :: E.TeamEvent -> TeamStore m () makeSem ''TeamStore diff --git a/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs new file mode 100644 index 00000000000..5a5f52468da --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs @@ -0,0 +1,333 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.TeamStore.Cassandra + ( interpretTeamStoreToCassandra, + ) +where + +import Cassandra +import Cassandra.Util +import Control.Lens hiding ((<|)) +import Control.Monad.Catch () +import Data.ByteString.Conversion (toByteString') +import Data.Id as Id +import Data.Json.Util (UTCTimeMillis (..), toUTCTimeMillis) +import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) +import Data.Map.Strict qualified as Map +import Data.Range +import Data.Set qualified as Set +import Data.Text.Encoding +import Data.UUID.V4 (nextRandom) +import Imports hiding (Set, max) +import Polysemy +import Polysemy.Input +import Polysemy.TinyLog +import UnliftIO qualified +import Wire.API.Routes.Internal.Galley.TeamsIntra +import Wire.API.Team +import Wire.API.Team.Member +import Wire.API.Team.Member.Info (TeamMemberInfo (TeamMemberInfo)) +import Wire.API.Team.Member.Info qualified as Info +import Wire.API.Team.Permission (Perm (SetBilling), Permissions, self) +import Wire.ConversationStore (ConversationStore) +import Wire.ConversationStore qualified as E +import Wire.ConversationStore.Cassandra.Instances () +import Wire.TeamStore (TeamStore (..)) +import Wire.TeamStore.Cassandra.Queries qualified as Cql +import Wire.Util (embedClientInput, logEffect) + +interpretTeamStoreToCassandra :: + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member TinyLog r, + Member ConversationStore r + ) => + Sem (TeamStore ': r) a -> + Sem r a +interpretTeamStoreToCassandra = interpret $ \case + CreateTeamMember tid mem -> do + logEffect "TeamStore.CreateTeamMember" + embedClientInput (addTeamMember tid mem) + SetTeamMemberPermissions perm0 tid uid perm1 -> do + logEffect "TeamStore.SetTeamMemberPermissions" + embedClientInput (updateTeamMember perm0 tid uid perm1) + CreateTeam t uid n i k b -> do + logEffect "TeamStore.CreateTeam" + createTeam t uid n i k b + DeleteTeamMember tid uid -> do + logEffect "TeamStore.DeleteTeamMember" + embedClientInput (removeTeamMember tid uid) + GetBillingTeamMembers tid -> do + logEffect "TeamStore.GetBillingTeamMembers" + embedClientInput (listBillingTeamMembers tid) + GetTeamAdmins tid -> do + logEffect "TeamStore.GetTeamAdmins" + embedClientInput (listTeamAdmins tid) + GetTeam tid -> do + logEffect "TeamStore.GetTeam" + embedClientInput (team tid) + GetTeamName tid -> do + logEffect "TeamStore.GetTeamName" + embedClientInput (getTeamName tid) + SelectTeams uid tids -> do + logEffect "TeamStore.SelectTeams" + embedClientInput (teamIdsOf uid tids) + GetTeamMember tid uid -> do + logEffect "TeamStore.GetTeamMember" + teamMember tid uid + GetTeamMembers tid -> do + logEffect "TeamStore.GetTeamMembers" + teamMembersCollectedWithPagination tid + GetTeamMembersWithLimit tid n -> do + logEffect "TeamStore.GetTeamMembersWithLimit" + teamMembersWithLimit tid n + SelectTeamMembers tid uids -> do + logEffect "TeamStore.SelectTeamMembers" + teamMembersLimited tid uids + SelectTeamMemberInfos tid uids -> do + logEffect "TeamStore.SelectTeamMemberInfos" + embedClientInput (teamMemberInfos tid uids) + GetUserTeams uid -> do + logEffect "TeamStore.GetUserTeams" + embedClientInput (userTeams uid) + GetUsersTeams uids -> do + logEffect "TeamStore.GetUsersTeams" + embedClientInput (usersTeams uids) + GetOneUserTeam uid -> do + logEffect "TeamStore.GetOneUserTeam" + embedClientInput (oneUserTeam uid) + GetTeamsBindings tid -> do + logEffect "TeamStore.GetTeamsBindings" + embedClientInput (getTeamsBindings tid) + GetTeamBinding tid -> do + logEffect "TeamStore.GetTeamBinding" + embedClientInput (getTeamBinding tid) + GetTeamCreationTime tid -> do + logEffect "TeamStore.GetTeamCreationTime" + embedClientInput (teamCreationTime tid) + DeleteTeam tid -> do + logEffect "TeamStore.DeleteTeam" + deleteTeam tid + SetTeamData tid upd -> do + logEffect "TeamStore.SetTeamData" + embedClientInput (updateTeam tid upd) + SetTeamStatus tid st -> do + logEffect "TeamStore.SetTeamStatus" + embedClientInput (updateTeamStatus tid st) + +createTeam :: + ( Member (Input ClientState) r, + Member (Embed IO) r + ) => + Maybe TeamId -> + UserId -> + Range 1 256 Text -> + Icon -> + Maybe (Range 1 256 Text) -> + TeamBinding -> + Sem r Team +createTeam t uid (fromRange -> n) i k b = do + tid <- embed @IO $ maybe (Id <$> liftIO nextRandom) pure t + embedClientInput $ retry x5 $ write Cql.insertTeam (params LocalQuorum (tid, uid, n, i, fromRange <$> k, initialStatus b, b)) + pure (newTeam tid uid n i b & teamIconKey .~ (fromRange <$> k)) + where + initialStatus Binding = PendingActive + initialStatus NonBinding = Active + +listBillingTeamMembers :: TeamId -> Client [UserId] +listBillingTeamMembers tid = fmap runIdentity <$> retry x1 (query Cql.listBillingTeamMembers (params LocalQuorum (Identity tid))) + +listTeamAdmins :: TeamId -> Client [UserId] +listTeamAdmins tid = fmap runIdentity <$> retry x1 (query Cql.listTeamAdmins (params LocalQuorum (Identity tid))) + +getTeamName :: TeamId -> Client (Maybe Text) +getTeamName tid = fmap runIdentity <$> retry x1 (query1 Cql.selectTeamName (params LocalQuorum (Identity tid))) + +teamIdsOf :: UserId -> [TeamId] -> Client [TeamId] +teamIdsOf uid tids = fmap runIdentity <$> retry x1 (query Cql.selectUserTeamsIn (params LocalQuorum (uid, tids))) + +team :: TeamId -> Client (Maybe TeamData) +team tid = fmap toTeam <$> retry x1 (query1 Cql.selectTeam (params LocalQuorum (Identity tid))) + where + toTeam (u, n, i, k, d, s, st, b, ss) = + let t = newTeam tid u n i (fromMaybe NonBinding b) & teamIconKey .~ k & teamSplashScreen .~ fromMaybe DefaultIcon ss + status = if d then PendingDelete else fromMaybe Active s + in TeamData t status (writetimeToUTC <$> st) + +teamMember :: + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => + TeamId -> + UserId -> + Sem r (Maybe TeamMember) +teamMember t u = do + mres <- embedClientInput $ retry x1 (query1 Cql.selectTeamMember (params LocalQuorum (t, u))) + pure $ fmap (\(perms, minvu, minvt, mulhStatus) -> newTeamMember' (u, perms, minvu, minvt, mulhStatus)) mres + +addTeamMember :: TeamId -> TeamMember -> Client () +addTeamMember t m = + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery Cql.insertTeamMember (t, m ^. userId, m ^. permissions, m ^? invitation . _Just . _1, m ^? invitation . _Just . _2) + addPrepQuery Cql.insertUserTeam (m ^. userId, t) + when (m `hasPermission` SetBilling) $ addPrepQuery Cql.insertBillingTeamMember (t, m ^. userId) + when (isAdminOrOwner (m ^. permissions)) $ addPrepQuery Cql.insertTeamAdmin (t, m ^. userId) + +updateTeamMember :: Permissions -> TeamId -> UserId -> Permissions -> Client () +updateTeamMember oldPerms tid uid newPerms = do + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery Cql.updatePermissions (newPerms, tid, uid) + let permDiff = Set.difference `on` self + acquiredPerms = newPerms `permDiff` oldPerms + lostPerms = oldPerms `permDiff` newPerms + when (SetBilling `Set.member` acquiredPerms) $ addPrepQuery Cql.insertBillingTeamMember (tid, uid) + when (SetBilling `Set.member` lostPerms) $ addPrepQuery Cql.deleteBillingTeamMember (tid, uid) + when (isAdminOrOwner newPerms && not (isAdminOrOwner oldPerms)) $ addPrepQuery Cql.insertTeamAdmin (tid, uid) + when (isAdminOrOwner oldPerms && not (isAdminOrOwner newPerms)) $ addPrepQuery Cql.deleteTeamAdmin (tid, uid) + +removeTeamMember :: TeamId -> UserId -> Client () +removeTeamMember tid uid = do + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery Cql.deleteTeamMember (tid, uid) + addPrepQuery Cql.deleteUserTeam (uid, tid) + addPrepQuery Cql.deleteBillingTeamMember (tid, uid) + addPrepQuery Cql.deleteTeamAdmin (tid, uid) + +userTeams :: UserId -> Client [TeamId] +userTeams u = map runIdentity <$> retry x1 (query Cql.selectUserTeams (params LocalQuorum (Identity u))) + +usersTeams :: [UserId] -> Client (Map UserId TeamId) +usersTeams uids = do + pairs :: [(UserId, TeamId)] <- catMaybes <$> UnliftIO.pooledMapConcurrentlyN 8 (\uid -> (uid,) <$$> oneUserTeam uid) uids + pure $ foldl' (\m (k, v) -> Map.insert k v m) Map.empty pairs + +oneUserTeam :: UserId -> Client (Maybe TeamId) +oneUserTeam u = fmap runIdentity <$> retry x1 (query1 Cql.selectOneUserTeam (params LocalQuorum (Identity u))) + +teamCreationTime :: TeamId -> Client (Maybe TeamCreationTime) +teamCreationTime t = checkCreation . fmap runIdentity <$> retry x1 (query1 Cql.selectTeamBindingWritetime (params LocalQuorum (Identity t))) + where + checkCreation (Just (Just ts)) = Just $ TeamCreationTime ts + checkCreation _ = Nothing + +getTeamBinding :: TeamId -> Client (Maybe TeamBinding) +getTeamBinding t = fmap (fromMaybe NonBinding . runIdentity) <$> retry x1 (query1 Cql.selectTeamBinding (params LocalQuorum (Identity t))) + +getTeamsBindings :: [TeamId] -> Client [TeamBinding] +getTeamsBindings = fmap catMaybes . UnliftIO.pooledMapConcurrentlyN 8 getTeamBinding + +deleteTeam :: + ( Member (Input ClientState) r, + Member (Embed IO) r, + Member ConversationStore r + ) => + TeamId -> + Sem r () +deleteTeam tid = do + embedClientInput (markTeamDeletedAndRemoveTeamMembers tid) + E.deleteTeamConversations tid + embedClientInput (retry x5 $ write Cql.deleteTeam (params LocalQuorum (Deleted, tid))) + +markTeamDeletedAndRemoveTeamMembers :: TeamId -> Client () +markTeamDeletedAndRemoveTeamMembers tid = do + retry x5 $ write Cql.markTeamDeleted (params LocalQuorum (PendingDelete, tid)) + mems <- teamMembersForPagination tid Nothing (unsafeRange 2000) + removeTeamMembers mems + where + removeTeamMembers mems = do + mapM_ (removeTeamMember tid . view _1) (result mems) + unless (null $ result mems) $ removeTeamMembers =<< liftClient (nextPage mems) + +updateTeamStatus :: TeamId -> TeamStatus -> Client () +updateTeamStatus t s = retry x5 $ write Cql.updateTeamStatus (params LocalQuorum (s, t)) + +updateTeam :: TeamId -> TeamUpdateData -> Client () +updateTeam tid u = retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + for_ (u ^. nameUpdate) $ \n -> addPrepQuery Cql.updateTeamName (fromRange n, tid) + for_ (u ^. iconUpdate) $ \i -> addPrepQuery Cql.updateTeamIcon (decodeUtf8 . toByteString' $ i, tid) + for_ (u ^. iconKeyUpdate) $ \k -> addPrepQuery Cql.updateTeamIconKey (fromRange k, tid) + for_ (u ^. splashScreenUpdate) $ \ss -> addPrepQuery Cql.updateTeamSplashScreen (decodeUtf8 . toByteString' $ ss, tid) + +newTeamMember' :: + (UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -> + TeamMember +newTeamMember' (uid, perms, mInvUser, mInvTime, fromMaybe defUserLegalHoldStatus -> lhStatus) = + mkTeamMember uid perms ((,) <$> mInvUser <*> mInvTime) lhStatus + +type RawTeamMember = (UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) + +teamMembersForPagination :: TeamId -> Maybe UserId -> Range 1 HardTruncationLimit Int32 -> Client (Page RawTeamMember) +teamMembersForPagination tid start (fromRange -> max) = + case start of + Just u -> paginate Cql.selectTeamMembersFrom (paramsP LocalQuorum (tid, u) max) + Nothing -> paginate Cql.selectTeamMembers (paramsP LocalQuorum (Identity tid) max) + +teamMembersCollectedWithPagination :: + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => + TeamId -> + Sem r [TeamMember] +teamMembersCollectedWithPagination tid = do + mems <- embedClientInput $ teamMembersForPagination tid Nothing (unsafeRange 2000) + collect [] mems + where + collect acc page = do + let tMembers = map newTeamMember' (result page) + if hasMore page + then do + page' <- embedClientInput (nextPage page) + collect (tMembers ++ acc) page' + else pure (tMembers ++ acc) + +teamMembersWithLimit :: + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => + TeamId -> + Range 1 HardTruncationLimit Int32 -> + Sem r TeamMemberList +teamMembersWithLimit t (fromRange -> limit) = do + page <- embedClientInput $ retry x1 (paginate Cql.selectTeamMembers (paramsP LocalQuorum (Identity t) (limit + 1))) + let ms = map newTeamMember' . take (fromIntegral limit) $ result page + pure $ if hasMore page then newTeamMemberList ms ListTruncated else newTeamMemberList ms ListComplete + +teamMembersLimited :: + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => + TeamId -> + [UserId] -> + Sem r [TeamMember] +teamMembersLimited t u = do + rows <- embedClientInput $ retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) + pure $ map (\(uid, perms, _, minvu, minvt, mlh) -> newTeamMember' (uid, perms, minvu, minvt, mlh)) rows + +teamMemberInfos :: TeamId -> [UserId] -> Client [TeamMemberInfo] +teamMemberInfos t u = mkTeamMemberInfo <$$> retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) + where + mkTeamMemberInfo (uid, perms, permsWT, _, _, _) = + TeamMemberInfo {Info.userId = uid, Info.permissions = perms, Info.permissionsWriteTime = toUTCTimeMillis $ writetimeToUTC permsWT} diff --git a/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs new file mode 100644 index 00000000000..921c718030f --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs @@ -0,0 +1,180 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.TeamStore.Cassandra.Queries where + +import Cassandra as C hiding (Value) +import Cassandra.Util (Writetime) +import Data.Id +import Data.Json.Util +import Data.LegalHold +import Imports +import Text.RawString.QQ +import Wire.API.Routes.Internal.Galley.TeamsIntra +import Wire.API.Team +import Wire.API.Team.Permission + +-- Teams -------------------------------------------------------------------- + +selectTeam :: PrepQuery R (Identity TeamId) (UserId, Text, Icon, Maybe Text, Bool, Maybe TeamStatus, Maybe (Writetime TeamStatus), Maybe TeamBinding, Maybe Icon) +selectTeam = "select creator, name, icon, icon_key, deleted, status, writetime(status), binding, splash_screen from team where team = ?" + +selectTeamName :: PrepQuery R (Identity TeamId) (Identity Text) +selectTeamName = "select name from team where team = ?" + +selectTeamBinding :: PrepQuery R (Identity TeamId) (Identity (Maybe TeamBinding)) +selectTeamBinding = "select binding from team where team = ?" + +selectTeamBindingWritetime :: PrepQuery R (Identity TeamId) (Identity (Maybe Int64)) +selectTeamBindingWritetime = "select writetime(binding) from team where team = ?" + +selectTeamMember :: + PrepQuery + R + (TeamId, UserId) + ( Permissions, + Maybe UserId, + Maybe UTCTimeMillis, + Maybe UserLegalHoldStatus + ) +selectTeamMember = "select perms, invited_by, invited_at, legalhold_status from team_member where team = ? and user = ?" + +selectTeamMembersBase :: (IsString a) => [String] -> a +selectTeamMembersBase conds = fromString $ selectFrom <> " where team = ?" <> whereClause <> " order by user" + where + selectFrom = "select user, perms, invited_by, invited_at, legalhold_status from team_member" + whereClause = concatMap (" and " <>) conds + +-- | This query fetches all members of a team, should be paginated. +selectTeamMembers :: + PrepQuery + R + (Identity TeamId) + ( UserId, + Permissions, + Maybe UserId, + Maybe UTCTimeMillis, + Maybe UserLegalHoldStatus + ) +selectTeamMembers = selectTeamMembersBase [] + +selectTeamMembersFrom :: + PrepQuery + R + (TeamId, UserId) + ( UserId, + Permissions, + Maybe UserId, + Maybe UTCTimeMillis, + Maybe UserLegalHoldStatus + ) +selectTeamMembersFrom = selectTeamMembersBase ["user > ?"] + +selectTeamMembers' :: + PrepQuery + R + (TeamId, [UserId]) + ( UserId, + Permissions, + Writetime Permissions, + Maybe UserId, + Maybe UTCTimeMillis, + Maybe UserLegalHoldStatus + ) +selectTeamMembers' = + [r| + select user, perms, writetime(perms), invited_by, invited_at, legalhold_status + from team_member + where team = ? and user in ? order by user + |] + +selectUserTeams :: PrepQuery R (Identity UserId) (Identity TeamId) +selectUserTeams = "select team from user_team where user = ? order by team" + +selectOneUserTeam :: PrepQuery R (Identity UserId) (Identity TeamId) +selectOneUserTeam = "select team from user_team where user = ? limit 1" + +selectUserTeamsIn :: PrepQuery R (UserId, [TeamId]) (Identity TeamId) +selectUserTeamsIn = "select team from user_team where user = ? and team in ? order by team" + +selectUserTeamsFrom :: PrepQuery R (UserId, TeamId) (Identity TeamId) +selectUserTeamsFrom = "select team from user_team where user = ? and team > ? order by team" + +insertTeam :: PrepQuery W (TeamId, UserId, Text, Icon, Maybe Text, TeamStatus, TeamBinding) () +insertTeam = "insert into team (team, creator, name, icon, icon_key, deleted, status, binding) values (?, ?, ?, ?, ?, false, ?, ?)" + +insertTeamMember :: PrepQuery W (TeamId, UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis) () +insertTeamMember = "insert into team_member (team, user, perms, invited_by, invited_at) values (?, ?, ?, ?, ?)" + +deleteTeamMember :: PrepQuery W (TeamId, UserId) () +deleteTeamMember = "delete from team_member where team = ? and user = ?" + +insertBillingTeamMember :: PrepQuery W (TeamId, UserId) () +insertBillingTeamMember = "insert into billing_team_member (team, user) values (?, ?)" + +deleteBillingTeamMember :: PrepQuery W (TeamId, UserId) () +deleteBillingTeamMember = "delete from billing_team_member where team = ? and user = ?" + +listBillingTeamMembers :: PrepQuery R (Identity TeamId) (Identity UserId) +listBillingTeamMembers = "select user from billing_team_member where team = ?" + +insertTeamAdmin :: PrepQuery W (TeamId, UserId) () +insertTeamAdmin = "insert into team_admin (team, user) values (?, ?)" + +deleteTeamAdmin :: PrepQuery W (TeamId, UserId) () +deleteTeamAdmin = "delete from team_admin where team = ? and user = ?" + +listTeamAdmins :: PrepQuery R (Identity TeamId) (Identity UserId) +listTeamAdmins = "select user from team_admin where team = ?" + +updatePermissions :: PrepQuery W (Permissions, TeamId, UserId) () +updatePermissions = "update team_member set perms = ? where team = ? and user = ?" + +insertUserTeam :: PrepQuery W (UserId, TeamId) () +insertUserTeam = "insert into user_team (user, team) values (?, ?)" + +deleteUserTeam :: PrepQuery W (UserId, TeamId) () +deleteUserTeam = "delete from user_team where user = ? and team = ?" + +markTeamDeleted :: PrepQuery W (TeamStatus, TeamId) () +markTeamDeleted = "update team set status = ? where team = ?" + +deleteTeam :: PrepQuery W (TeamStatus, TeamId) () +deleteTeam = "update team using timestamp 32503680000000000 set name = 'default', icon = 'default', status = ? where team = ? " + +updateTeamName :: PrepQuery W (Text, TeamId) () +updateTeamName = "update team set name = ? where team = ?" + +updateTeamIcon :: PrepQuery W (Text, TeamId) () +updateTeamIcon = "update team set icon = ? where team = ?" + +updateTeamIconKey :: PrepQuery W (Text, TeamId) () +updateTeamIconKey = "update team set icon_key = ? where team = ?" + +updateTeamStatus :: PrepQuery W (TeamStatus, TeamId) () +updateTeamStatus = "update team set status = ? where team = ?" + +updateTeamSplashScreen :: PrepQuery W (Text, TeamId) () +updateTeamSplashScreen = "update team set splash_screen = ? where team = ?" + +-- LegalHold whitelist ------------------------------------------------------- + +selectLegalHoldWhitelistedTeam :: PrepQuery R (Identity TeamId) (Identity TeamId) +selectLegalHoldWhitelistedTeam = + [r| + select team from legalhold_whitelisted where team = ? + |] diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem.hs index 9bd7d5ea923..62914e0cea7 100644 --- a/libs/wire-subsystems/src/Wire/TeamSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem.hs @@ -20,6 +20,7 @@ module Wire.TeamSubsystem where import Data.Id +import Data.Qualified import Data.Range import Imports import Polysemy @@ -28,8 +29,10 @@ import Wire.API.Team.Member.Info (TeamMemberInfoList) data TeamSubsystem m a where InternalGetTeamMember :: UserId -> TeamId -> TeamSubsystem m (Maybe TeamMember) - InternalGetTeamMembers :: TeamId -> Maybe (Range 1 HardTruncationLimit Int32) -> TeamSubsystem m TeamMemberList + InternalGetTeamMembersWithLimit :: TeamId -> Maybe (Range 1 HardTruncationLimit Int32) -> TeamSubsystem m TeamMemberList + InternalSelectTeamMembers :: TeamId -> [UserId] -> TeamSubsystem m [TeamMember] InternalSelectTeamMemberInfos :: TeamId -> [UserId] -> TeamSubsystem m TeamMemberInfoList InternalGetTeamAdmins :: TeamId -> TeamSubsystem m TeamMemberList + InternalFinalizeDeleteTeam :: Local UserId -> Maybe ConnId -> TeamId -> TeamSubsystem m () makeSem ''TeamSubsystem diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem/GalleyAPI.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem/GalleyAPI.hs index d5c51b3dd0d..d34edaf4045 100644 --- a/libs/wire-subsystems/src/Wire/TeamSubsystem/GalleyAPI.hs +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem/GalleyAPI.hs @@ -23,9 +23,11 @@ import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.TeamSubsystem -intepreterTeamSubsystemToGalleyAPI :: (Member GalleyAPIAccess r) => InterpreterFor TeamSubsystem r -intepreterTeamSubsystemToGalleyAPI = interpret $ \case +interpretTeamSubsystemToGalleyAPI :: (Member GalleyAPIAccess r) => InterpreterFor TeamSubsystem r +interpretTeamSubsystemToGalleyAPI = interpret $ \case InternalGetTeamMember userId teamId -> GalleyAPIAccess.getTeamMember userId teamId - InternalGetTeamMembers teamId maxResults -> GalleyAPIAccess.getTeamMembers teamId maxResults + InternalGetTeamMembersWithLimit teamId maxResults -> GalleyAPIAccess.getTeamMembersWithLimit teamId maxResults InternalSelectTeamMemberInfos teamId userIds -> GalleyAPIAccess.selectTeamMemberInfos teamId userIds + InternalSelectTeamMembers teamId userIds -> GalleyAPIAccess.selectTeamMembers teamId userIds InternalGetTeamAdmins teamId -> GalleyAPIAccess.getTeamAdmins teamId + InternalFinalizeDeleteTeam lusr mcon teamId -> GalleyAPIAccess.finalizeDeleteTeam lusr mcon teamId diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem/Interpreter.hs new file mode 100644 index 00000000000..d0f994ab3c2 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem/Interpreter.hs @@ -0,0 +1,205 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.TeamSubsystem.Interpreter where + +import Control.Lens (view, (%~), (^.)) +import Data.Default +import Data.Id +import Data.Json.Util +import Data.LegalHold (UserLegalHoldStatus (..)) +import Data.List.Extra qualified as List +import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.Qualified +import Data.Time +import Imports +import Polysemy +import Polysemy.Input +import Wire.API.Event.Conversation qualified as Conv +import Wire.API.Event.Team +import Wire.API.Team.HardTruncationLimit +import Wire.API.Team.Member +import Wire.API.Team.Member.Info (TeamMemberInfoList (TeamMemberInfoList)) +import Wire.BrigAPIAccess +import Wire.BrigAPIAccess qualified as Brig +import Wire.ConversationStore +import Wire.ConversationStore qualified as ConvStore +import Wire.ExternalAccess +import Wire.ExternalAccess qualified as ExternalAccess +import Wire.LegalHoldStore (LegalHoldStore) +import Wire.LegalHoldStore qualified as LH +import Wire.NotificationSubsystem +import Wire.Sem.Now +import Wire.Sem.Now qualified as Now +import Wire.SparAPIAccess +import Wire.SparAPIAccess qualified as Spar +import Wire.StoredConversation +import Wire.TeamJournal +import Wire.TeamJournal qualified as Journal +import Wire.TeamStore (TeamStore) +import Wire.TeamStore qualified as TeamStore +import Wire.TeamSubsystem + +newtype TeamSubsystemConfig = TeamSubsystemConfig {concurrentDeletionEvents :: Int} + +interpretTeamSubsystem :: + ( Member TeamStore r, + Member LegalHoldStore r, + Member BrigAPIAccess r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member Now r, + Member SparAPIAccess r, + Member ConversationStore r, + Member TeamJournal r + ) => + TeamSubsystemConfig -> + InterpreterFor TeamSubsystem r +interpretTeamSubsystem config = + runInputConst config . interpretTeamSubsystemWithInputConfig . raiseUnder + +interpretTeamSubsystemWithInputConfig :: + ( Member TeamStore r, + Member LegalHoldStore r, + Member BrigAPIAccess r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member (Input TeamSubsystemConfig) r, + Member Now r, + Member SparAPIAccess r, + Member ConversationStore r, + Member TeamJournal r + ) => + InterpreterFor TeamSubsystem r +interpretTeamSubsystemWithInputConfig = + interpret $ \case + InternalGetTeamMember uid tid -> do + tms <- TeamStore.getTeamMember tid uid + for tms $ \tm -> do + hasImplicitConsent <- LH.isTeamLegalholdWhitelisted tid + pure $ if hasImplicitConsent then grantImplicitConsent tm else tm + InternalGetTeamMembersWithLimit tid maxResults -> do + tmList <- TeamStore.getTeamMembersWithLimit tid (fromMaybe hardTruncationLimitRange maxResults) + ms <- adjustMembersForImplicitConsent tid (tmList ^. teamMembers) + pure $ newTeamMemberList ms (tmList ^. teamMemberListType) + InternalSelectTeamMemberInfos tid uids -> TeamMemberInfoList <$> TeamStore.selectTeamMemberInfos tid uids + InternalSelectTeamMembers tid uids -> do + tms <- TeamStore.selectTeamMembers tid uids + adjustMembersForImplicitConsent tid tms + InternalGetTeamAdmins tid -> do + admins <- + TeamStore.getTeamAdmins tid + >>= TeamStore.selectTeamMembers tid + >>= adjustMembersForImplicitConsent tid + pure $ newTeamMemberList admins ListComplete + InternalFinalizeDeleteTeam luid mcon tid -> + internalFinalizeDeleteTeamImpl luid mcon tid + +adjustMembersForImplicitConsent :: (Member LegalHoldStore r) => TeamId -> [TeamMember] -> Sem r [TeamMember] +adjustMembersForImplicitConsent tid ms = do + hasImplicitConsent <- LH.isTeamLegalholdWhitelisted tid + pure $ if hasImplicitConsent then map grantImplicitConsent ms else ms + +grantImplicitConsent :: TeamMember -> TeamMember +grantImplicitConsent = + legalHoldStatus %~ \case + UserLegalHoldNoConsent -> UserLegalHoldDisabled + UserLegalHoldDisabled -> UserLegalHoldDisabled + UserLegalHoldPending -> UserLegalHoldPending + UserLegalHoldEnabled -> UserLegalHoldEnabled + +-- This function is "unchecked" because it does not validate that the user has the `DeleteTeam` permission. +internalFinalizeDeleteTeamImpl :: + forall r. + ( Member BrigAPIAccess r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member (Input TeamSubsystemConfig) r, + Member Now r, + Member LegalHoldStore r, + Member SparAPIAccess r, + Member TeamStore r, + Member ConversationStore r, + Member TeamJournal r + ) => + Local UserId -> + Maybe ConnId -> + TeamId -> + Sem r () +internalFinalizeDeleteTeamImpl lusr zcon tid = do + team <- TeamStore.getTeam tid + when (isJust team) $ do + Spar.deleteTeam tid + now <- Now.get + convs <- ConvStore.getTeamConversations tid + -- Even for LARGE TEAMS, we _DO_ want to fetch all team members here because we + -- want to generate conversation deletion events for non-team users. This should + -- be fine as it is done once during the life team of a team and we still do not + -- fanout this particular event to all team members anyway. And this is anyway + -- done asynchronously + + -- No need to adjust implicit consent here. + membs <- TeamStore.getTeamMembers tid + (ue, be) <- foldrM (createConvDeleteEvents now membs) ([], []) convs + let e = newEvent tid now EdTeamDelete + pushDeleteEvents membs e ue + ExternalAccess.deliverAsync be + -- TODO: we don't delete bots here, but we should do that, since + -- every bot user can only be in a single conversation. Just + -- deleting conversations from the database is not enough. + mapM_ (Brig.deleteUser . view userId) membs + Journal.teamDelete tid + LH.unsetTeamLegalholdWhitelisted tid + TeamStore.deleteTeam tid + where + pushDeleteEvents :: [TeamMember] -> Event -> [Push] -> Sem r () + pushDeleteEvents membs e ue = do + let r = userRecipient (tUnqualified lusr) :| membersToRecipients (Just (tUnqualified lusr)) membs + + -- To avoid DoS on gundeck, send team deletion events in chunks + chunkSize <- inputs (.concurrentDeletionEvents) + let chunks = List.chunksOf chunkSize (toList r) + forM_ chunks $ \chunk -> + -- push TeamDelete events. Note that despite having a complete list, we are guaranteed in the + -- push module to never fan this out to more than the limit + pushNotifications [def {origin = Just (tUnqualified lusr), json = toJSONObject e, recipients = chunk, conn = zcon}] + -- To avoid DoS on gundeck, send conversation deletion events slowly + pushNotificationsSlowly ue + createConvDeleteEvents :: + UTCTime -> + [TeamMember] -> + ConvId -> + ([Push], [(BotMember, Conv.Event)]) -> + Sem r ([Push], [(BotMember, Conv.Event)]) + createConvDeleteEvents now teamMembs cid (pp, ee) = do + let qconvId = tUntagged $ qualifyAs lusr cid + (bots, convMembs) <- localBotsAndUsers <$> ConvStore.getLocalMembers cid + -- Only nonTeamMembers need to get any events, since on team deletion, + -- all team users are deleted immediately after these events are sent + -- and will thus never be able to see these events in practice. + let mm = nonTeamMembers convMembs teamMembs + let e = Conv.Event qconvId Nothing (Conv.EventFromUser (tUntagged lusr)) now (Just tid) Conv.EdConvDelete + -- This event always contains all the required recipients + let p = + def + { origin = Just (tUnqualified lusr), + json = toJSONObject e, + recipients = map localMemberToRecipient mm + } + let ee' = map (,e) bots + let pp' = (p {conn = zcon}) : pp + pure (pp', ee' ++ ee) diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index e8eb7bcb25b..a0d20c10702 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -305,7 +305,7 @@ miniBackendLowerEffectsInterpreters mb@(MiniBackendParams {..}) = . miniGalleyAPIAccess teams galleyConfigs . inMemoryNotificationSubsystemInterpreter . noopEmailSubsystemInterpreter - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI type StateEffects = '[ State [Push], diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs index 3224c6cc972..2d448a620f5 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs @@ -46,7 +46,7 @@ miniGalleyAPIAccess teams configs = interpret $ \case AddTeamMember {} -> error "AddTeamMember not implemented in miniGalleyAPIAccess" CreateTeam {} -> error "CreateTeam not implemented in miniGalleyAPIAccess" GetTeamMember uid tid -> pure $ getTeamMemberImpl teams uid tid - GetTeamMembers tid maxResults -> pure $ getTeamMembersImpl teams tid maxResults + GetTeamMembersWithLimit tid maxResults -> pure $ getTeamMembersImpl teams tid maxResults GetTeamId _ -> error "GetTeamId not implemented in miniGalleyAPIAccess" GetTeam _ -> error "GetTeam not implemented in miniGalleyAPIAccess" GetTeamName _ -> error "GetTeamName not implemented in miniGalleyAPIAccess" @@ -55,6 +55,7 @@ miniGalleyAPIAccess teams configs = interpret $ \case GetTeamSearchVisibility _ -> pure SearchVisibilityStandard ChangeTeamStatus {} -> error "ChangeTeamStatus not implemented in miniGalleyAPIAccess" + FinalizeDeleteTeam {} -> error "FinalizeDeleteTeam not implemented in miniGalleyAPIAccess" MemberIsTeamOwner tid uid -> pure $ memberIsTeamOwnerImpl teams tid uid GetAllTeamFeaturesForUser _ -> pure configs @@ -68,6 +69,7 @@ miniGalleyAPIAccess teams configs = interpret $ \case SelectTeamMemberInfos tid uids -> pure $ selectTeamMemberInfosImpl teams tid uids InternalGetConversation _ -> error "GetConv not implemented in InternalGetConversation" GetTeamContacts _ -> pure Nothing + SelectTeamMembers {} -> error "SelectTeamMembers not implemented in miniGalleyAPIAccess" -- this is called but the result is not needed in unit tests selectTeamMemberInfosImpl :: Map TeamId [TeamMember] -> TeamId -> [UserId] -> TeamMemberInfoList diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs index eff9c2b70ae..4122706e412 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs @@ -30,6 +30,8 @@ miniSparAPIAccess :: (Member (Input (Map TeamId IdPList)) r) => InterpreterFor S miniSparAPIAccess = interpret $ \case GetIdentityProviders tid -> Map.findWithDefault (IdPList []) tid <$> input + DeleteTeam {} -> error "DeleteTeam not implemented in miniSparAPIAccess" + LookupScimUserInfo {} -> error "LookupScimUserInfo not implemented in miniSparAPIAccess" emptySparAPIAccess :: InterpreterFor SparAPIAccess r emptySparAPIAccess = runInputConst mempty . miniSparAPIAccess . raiseUnder diff --git a/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs index ea2f5faf711..0e7c71ca1f7 100644 --- a/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs @@ -97,7 +97,7 @@ runAllEffects args = . evalState (mkStdGen 3) . randomToStatefulStdGen . miniGalleyAPIAccess args.teams def - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI . discardTinyLogs . enterpriseLoginSubsystemTestInterpreter args.constGuardResult . runError diff --git a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs index 9f21da1caef..d163e0b8122 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs @@ -109,7 +109,7 @@ interpretDependencies initialUsers initialTeams = . runInputConst (toLocalUnsafe (Domain "example.com") ()) . runInMemoryUserGroupStore def . miniGalleyAPIAccess initialTeams def - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI . userSubsystemTestInterpreter initialUsers runDependenciesWithReturnState :: @@ -128,7 +128,7 @@ runDependenciesWithReturnState initialUsers initialTeams = . runInputConst (toLocalUnsafe (Domain "example.com") ()) . runInMemoryUserGroupStore def . miniGalleyAPIAccess initialTeams def - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI . userSubsystemTestInterpreter initialUsers expectRight :: (Show err) => Either err Property -> Property diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 96866075479..13c3773a0db 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -80,6 +80,7 @@ common common-all , amazonka , amazonka-core , amazonka-ses + , amazonka-sqs , amqp , async , attoparsec @@ -106,6 +107,7 @@ common common-all , extended , extra , file-embed + , galley-types , hashable , HaskellNet , HaskellNet-SSL @@ -141,6 +143,7 @@ common common-all , polysemy-wire-zoo , profunctors , prometheus-client + , proto-lens , QuickCheck , raw-strings-qq , resource-pool @@ -166,6 +169,7 @@ common common-all , token-bucket , transformers , types-common + , types-common-journal , unliftio , unordered-containers , uri-bytestring @@ -263,6 +267,10 @@ library Wire.InternalEvent Wire.InvitationStore Wire.InvitationStore.Cassandra + Wire.LegalHoldStore + Wire.LegalHoldStore.Cassandra + Wire.LegalHoldStore.Cassandra.Queries + Wire.LegalHoldStore.Env Wire.ListItems Wire.NotificationSubsystem Wire.NotificationSubsystem.Interpreter @@ -298,8 +306,14 @@ library Wire.TeamInvitationSubsystem Wire.TeamInvitationSubsystem.Error Wire.TeamInvitationSubsystem.Interpreter + Wire.TeamJournal + Wire.TeamJournal.Aws + Wire.TeamStore + Wire.TeamStore.Cassandra + Wire.TeamStore.Cassandra.Queries Wire.TeamSubsystem Wire.TeamSubsystem.GalleyAPI + Wire.TeamSubsystem.Interpreter Wire.TeamSubsystem.Util Wire.UserGroupStore Wire.UserGroupStore.Postgres @@ -405,6 +419,7 @@ library , time-out , time-units , tinylog + , tls , token-bucket , transformers , types-common diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index c64ccdbcb7e..51100f5867a 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -65,7 +65,6 @@ import Brig.IO.Intra (guardLegalhold) import Brig.IO.Intra qualified as Intra import Brig.Options qualified as Opt import Brig.Types.Intra -import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.User.Auth qualified as UserAuth import Brig.User.Auth.Cookie qualified as Auth import Cassandra (MonadClient) @@ -98,6 +97,7 @@ import Wire.API.MLS.Credential (ClientIdentity (..)) import Wire.API.MLS.Epoch (addToEpoch) import Wire.API.Message qualified as Message import Wire.API.Team.LegalHold (LegalholdProtectee (..)) +import Wire.API.Team.LegalHold.Internal import Wire.API.User import Wire.API.User qualified as Code import Wire.API.User.Client diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 43636d59dda..e38db0c168d 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -44,7 +44,6 @@ import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team import Brig.Types.Connection import Brig.Types.Intra -import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User import Brig.User.EJPD qualified import Brig.User.Search.Index qualified as Search @@ -89,6 +88,7 @@ import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named import Wire.API.Team.Export import Wire.API.Team.Feature +import Wire.API.Team.LegalHold.Internal import Wire.API.User import Wire.API.User.Activation import Wire.API.User.Client diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index cce8eb2b8f0..1345b4d4cb0 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -67,7 +67,7 @@ import System.Logger.Class import UnliftIO.Async import UnliftIO.Exception import Util.Options -import Wire.AWS +import Wire.AWS (canRetry, sendCatch) data Env = Env { _logger :: !Logger, diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 72b5eb6a152..13bef0bfbc6 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -392,7 +392,7 @@ runBrigToIO e (AppT ma) = do . emailSubsystemInterpreter e.userTemplates e.teamTemplates e.templateBranding . interpretAppStoreToPostgres . interpretTeamCollaboratorsStoreToPostgres - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI . interpretTeamCollaboratorsSubsystem . userSubsystemInterpreter . interpretUserGroupSubsystem diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index ce9640f9746..5dc3a526fc9 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -55,7 +55,6 @@ import Amazonka.DynamoDB.Lens qualified as AWS import Bilge.Retry (httpHandlers) import Brig.AWS import Brig.App -import Brig.Types.Instances () import Cassandra as C hiding (Client) import Cassandra.Settings as C hiding (Client) import Control.Error diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index 2f0aecc3703..5279fcd9923 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -17,7 +17,6 @@ module Brig.Provider.DB where -import Brig.Types.Instances () import Brig.Types.Provider.Tag import Cassandra as C import Control.Arrow ((&&&)) @@ -33,7 +32,7 @@ import UnliftIO (mapConcurrently) import Wire.API.Password as Password import Wire.API.Provider import Wire.API.Provider.Service hiding (updateServiceTags) -import Wire.API.Provider.Service.Tag +import Wire.API.Provider.Service.Tag (QueryAllTags (..), QueryAnyTags (..)) import Wire.API.User import Wire.UserKeyStore diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 2af1e1e3444..5b8022812ef 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -421,7 +421,7 @@ changeTeamAccountStatuses tid s = do team <- Team.tdTeam <$> lift (liftSem $ GalleyAPIAccess.getTeam tid) unless (team ^. teamBinding == Binding) $ throwStd noBindingTeam - uids <- toNonEmpty =<< lift (fmap (view Teams.userId) . view teamMembers <$> liftSem (TeamSubsystem.internalGetTeamMembers tid Nothing)) + uids <- toNonEmpty =<< lift (fmap (view Teams.userId) . view teamMembers <$> liftSem (TeamSubsystem.internalGetTeamMembersWithLimit tid Nothing)) API.changeAccountStatus uids s !>> accountStatusError where toNonEmpty (x : xs) = pure $ x :| xs diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index a44e2923d0b..a4ee60709e8 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -104,7 +104,7 @@ ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do mbTeamContacts <- case (reallyIncludeContacts, userTeam target) of (True, Just tid) -> do - memberList <- liftSem $ TeamSubsystem.internalGetTeamMembers tid Nothing + memberList <- liftSem $ TeamSubsystem.internalGetTeamMembersWithLimit tid Nothing let members = (view Team.userId <$> (memberList ^. Team.teamMembers)) \\ [uid] contactsFull <- diff --git a/services/galley/default.nix b/services/galley/default.nix index e16ec2e48dc..1f6558900e0 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -6,7 +6,6 @@ , aeson , aeson-qq , amazonka -, amazonka-sqs , amqp , asn1-encoding , asn1-types @@ -32,7 +31,6 @@ , currency-codes , data-default , data-timeout -, enclosed-exceptions , errors , exceptions , extended @@ -77,7 +75,6 @@ , quickcheck-instances , random , raw-strings-qq -, resourcet , retry , safe-exceptions , servant @@ -101,7 +98,6 @@ , text , time , tinylog -, tls , transformers , types-common , types-common-aws @@ -135,13 +131,11 @@ mkDerivation { libraryHaskellDepends = [ aeson amazonka - amazonka-sqs amqp asn1-encoding asn1-types async base - base64-bytestring bilge brig-types bytestring @@ -153,10 +147,8 @@ mkDerivation { containers crypton crypton-x509 - currency-codes data-default data-timeout - enclosed-exceptions errors exceptions extended @@ -183,9 +175,7 @@ mkDerivation { polysemy-conc polysemy-wire-zoo prometheus-client - proto-lens raw-strings-qq - resourcet retry safe-exceptions servant @@ -201,11 +191,9 @@ mkDerivation { text time tinylog - tls transformers types-common types-common-aws - types-common-journal unliftio unordered-containers uri-bytestring diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 1207da8f177..69a7cf3f29d 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -136,12 +136,10 @@ library Galley.API.Update Galley.API.Util Galley.App - Galley.Aws Galley.Cassandra Galley.Cassandra.Client Galley.Cassandra.Code Galley.Cassandra.CustomBackend - Galley.Cassandra.LegalHold Galley.Cassandra.Proposal Galley.Cassandra.Queries Galley.Cassandra.SearchVisibility @@ -158,22 +156,16 @@ library Galley.Effects.CodeStore Galley.Effects.CustomBackendStore Galley.Effects.FederatorAccess - Galley.Effects.LegalHoldStore Galley.Effects.ProposalStore Galley.Effects.Queue Galley.Effects.SearchVisibilityStore - Galley.Effects.SparAccess Galley.Effects.TeamFeatureStore Galley.Effects.TeamMemberStore Galley.Effects.TeamNotificationStore - Galley.Effects.TeamStore Galley.Env Galley.External.LegalHoldService Galley.External.LegalHoldService.Internal - Galley.Intra.Effects Galley.Intra.Federator - Galley.Intra.Journal - Galley.Intra.Spar Galley.Intra.Util Galley.Keys Galley.Monad @@ -262,7 +254,6 @@ library Galley.Schema.V97_CellsConversation Galley.Schema.V98_ChannelAddPermission Galley.Schema.V99_ConversationAddParent - Galley.TeamSubsystem Galley.Types.Clients Galley.Validation @@ -272,13 +263,11 @@ library build-depends: , aeson >=2.0.1.0 , amazonka >=1.4.5 - , amazonka-sqs >=1.4.5 , amqp , asn1-encoding , asn1-types , async >=2.0 , base >=4.6 && <5 - , base64-bytestring >=1.0 , bilge >=0.21.1 , brig-types >=0.73.1 , bytestring >=0.9 @@ -290,10 +279,8 @@ library , containers >=0.5 , crypton , crypton-x509 - , currency-codes >=2.0 , data-default , data-timeout - , enclosed-exceptions >=1.0 , errors >=2.0 , exceptions >=0.4 , extended @@ -320,9 +307,7 @@ library , polysemy-conc , polysemy-wire-zoo , prometheus-client - , proto-lens >=0.2 , raw-strings-qq >=1.0 - , resourcet >=1.1 , retry >=0.5 , safe-exceptions >=0.1 , servant @@ -338,11 +323,9 @@ library , text >=0.11 , time >=1.4 , tinylog >=0.10 - , tls >=1.7.0 , transformers , types-common >=0.16 , types-common-aws - , types-common-journal >=0.1 , unliftio >=0.2 , unordered-containers , uri-bytestring >=0.2 diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index f096a394a86..1f45ab06963 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -80,7 +80,6 @@ import Galley.Effects import Galley.Effects.CodeStore qualified as E import Galley.Effects.FederatorAccess qualified as E import Galley.Effects.ProposalStore qualified as E -import Galley.Effects.TeamStore qualified as E import Galley.Env (Env) import Galley.Options import Galley.Validation @@ -127,6 +126,8 @@ import Wire.Sem.Now qualified as Now import Wire.StoredConversation import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Constraint where @@ -424,7 +425,8 @@ ensureAllowed :: ( IsConvMember mem, HasConversationActionEffects tag r, Member (ErrorS ConvNotFound) r, - Member (Error FederationError) r + Member (Error FederationError) r, + Member TeamSubsystem r ) => Sing tag -> Local x -> @@ -450,7 +452,7 @@ ensureAllowed tag loc action conv (ActorContext (Just origUser) mTm) = do SConversationDeleteTag -> for_ (convTeam conv) $ \tid -> do lusr <- ensureLocal loc (convMemberId loc origUser) - void $ E.getTeamMember tid (tUnqualified lusr) >>= noteS @'NotATeamMember + void $ TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid >>= noteS @'NotATeamMember SConversationAccessDataTag -> do -- 'PrivateAccessRole' is for self-conversations, 1:1 conversations and -- so on; users not supposed to be able to make other conversations @@ -501,7 +503,8 @@ performAction :: Member TeamCollaboratorsSubsystem r, Member (Error FederationError) r, Member ConversationSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Sing tag -> Qualified UserId -> @@ -556,7 +559,7 @@ performAction tag origUser lconv action = do pure $ mkPerformActionResult action SConversationRenameTag -> do - zusrMembership <- join <$> forM conv.metadata.cnvmTeam (flip E.getTeamMember (qUnqualified origUser)) + zusrMembership <- join <$> forM conv.metadata.cnvmTeam (TeamSubsystem.internalGetTeamMember (qUnqualified origUser)) for_ zusrMembership $ \tm -> unless (tm `hasPermission` ModifyConvName) $ throwS @'InvalidOperation cn <- rangeChecked (cupName action) E.setConversationName (tUnqualified lcnv) cn @@ -618,7 +621,8 @@ performConversationJoin :: ( HasConversationActionEffects 'ConversationJoinTag r, Member BackendNotificationQueueAccess r, Member ConversationSubsystem r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Qualified UserId -> Local StoredConversation -> @@ -665,7 +669,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do checkLocals lusr (Just tid) newUsers = do tms <- Map.fromList . map (view Wire.API.Team.Member.userId &&& Imports.id) - <$> E.selectTeamMembers tid newUsers + <$> TeamSubsystem.internalSelectTeamMembers tid newUsers let userMembershipMap = map (Imports.id &&& flip Map.lookup tms) newUsers ensureAccessRole (convAccessRoles conv) userMembershipMap ensureConnectedToLocalsOrSameTeam lusr newUsers @@ -730,7 +734,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do checkTeamMemberAddPermission lusr = do case conv.metadata.cnvmTeam of Just tid -> do - maybeTeamMember <- E.getTeamMember tid (tUnqualified lusr) + maybeTeamMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid case maybeTeamMember of Just tm -> do let isChannel = conv.metadata.cnvmGroupConvType == Just Channel @@ -764,7 +768,8 @@ performConversationAccessData :: ( HasConversationActionEffects 'ConversationAccessDataTag r, Member (Error FederationError) r, Member BackendNotificationQueueAccess r, - Member ConversationSubsystem r + Member ConversationSubsystem r, + Member TeamSubsystem r ) => Qualified UserId -> Local StoredConversation -> @@ -822,23 +827,23 @@ performConversationAccessData qusr lconv action = do -- FUTUREWORK: should we also remove non-activated remote users? pure $ bm {bmLocals = Set.fromList activated} - maybeRemoveNonTeamMembers :: (Member TeamStore r) => BotsAndMembers -> Sem r BotsAndMembers + maybeRemoveNonTeamMembers :: (Member TeamSubsystem r) => BotsAndMembers -> Sem r BotsAndMembers maybeRemoveNonTeamMembers bm = if Set.member NonTeamMemberAccessRole (cupAccessRoles action) then pure bm else case convTeam conv of Just tid -> do - onlyTeamUsers <- filterM (fmap isJust . E.getTeamMember tid) (toList (bmLocals bm)) + onlyTeamUsers <- filterM (fmap isJust . flip TeamSubsystem.internalGetTeamMember tid) (toList (bmLocals bm)) pure $ bm {bmLocals = Set.fromList onlyTeamUsers, bmRemotes = mempty} Nothing -> pure bm - maybeRemoveTeamMembers :: (Member TeamStore r) => BotsAndMembers -> Sem r BotsAndMembers + maybeRemoveTeamMembers :: (Member TeamSubsystem r) => BotsAndMembers -> Sem r BotsAndMembers maybeRemoveTeamMembers bm = if Set.member TeamMemberAccessRole (cupAccessRoles action) then pure bm else case convTeam conv of Just tid -> do - noTeamMembers <- filterM (fmap isNothing . E.getTeamMember tid) (toList (bmLocals bm)) + noTeamMembers <- filterM (fmap isNothing . flip TeamSubsystem.internalGetTeamMember tid) (toList (bmLocals bm)) pure $ bm {bmLocals = Set.fromList noTeamMembers} Nothing -> pure bm @@ -853,9 +858,9 @@ updateLocalConversation :: Member ConversationSubsystem r, HasConversationActionEffects tag r, SingI tag, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local ConvId -> Qualified UserId -> @@ -888,9 +893,9 @@ updateLocalConversationUnchecked :: Member (ErrorS 'InvalidOperation) r, Member ConversationSubsystem r, HasConversationActionEffects tag r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local StoredConversation -> Qualified UserId -> @@ -914,7 +919,7 @@ updateLocalConversationUnchecked lconv qusr con action = do par.extraConversationData where getTeamMembership :: StoredConversation -> Local UserId -> Sem r (Maybe TeamMember) - getTeamMembership conv luid = maybe (pure Nothing) (`E.getTeamMember` tUnqualified luid) conv.metadata.cnvmTeam + getTeamMembership conv luid = maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified luid)) conv.metadata.cnvmTeam ensureConversationActionAllowed :: Sing tag -> Local x -> StoredConversation -> Maybe TeamMember -> Sem r () ensureConversationActionAllowed tag loc conv mTeamMember = do diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 9e14293b642..409f0249417 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -52,7 +52,6 @@ import Galley.API.Util import Galley.App (Env) import Galley.Effects import Galley.Effects.FederatorAccess qualified as E -import Galley.Effects.TeamStore qualified as E import Galley.Options import Galley.Types.Teams (notTeamMember) import Galley.Validation @@ -90,6 +89,9 @@ import Wire.Sem.Random qualified as Random import Wire.StoredConversation hiding (convTeam, localOne2OneConvId) import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore qualified as TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList ---------------------------------------------------------------------------- @@ -124,7 +126,8 @@ createGroupConversationUpToV3 :: Member P.TinyLog r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -170,7 +173,8 @@ createGroupOwnConversation :: Member P.TinyLog r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -216,7 +220,8 @@ createGroupConversation :: Member P.TinyLog r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -263,7 +268,8 @@ createGroupConvAndMkResponse :: Member TeamStore r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -305,7 +311,8 @@ createGroupConversationGeneric :: Member P.TinyLog r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -349,7 +356,8 @@ ensureNoLegalholdConflicts :: ( Member (ErrorS 'MissingLegalholdConsent) r, Member (Input Opts) r, Member LegalHoldStore r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => UserList UserId -> Sem r () @@ -370,7 +378,8 @@ checkCreateConvPermissions :: Member TeamStore r, Member (Input Opts) r, Member TeamFeatureStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> NewConv -> @@ -414,7 +423,7 @@ checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do when (length allUsers > 1 || newConv.newConvProtocol == BaseProtocolMLSTag) $ do void $ permissionCheck AddRemoveConvMember teamAssociation - convLocalMemberships <- mapM (E.getTeamMember convTeam) (ulLocals allUsers) + convLocalMemberships <- mapM (flip TeamSubsystem.internalGetTeamMember convTeam) (ulLocals allUsers) ensureAccessRole (accessRoles newConv) (zip (ulLocals allUsers) convLocalMemberships) -- Team members are always considered to be connected, so we only check -- 'ensureConnected' for non-team-members. @@ -444,9 +453,9 @@ checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do ensureCreateChannelPermissions _ Nothing = do throwS @NotATeamMember -getTeamMember :: (Member TeamStore r) => UserId -> Maybe TeamId -> Sem r (Maybe TeamMember) -getTeamMember uid (Just tid) = E.getTeamMember tid uid -getTeamMember uid Nothing = E.getUserTeams uid >>= maybe (pure Nothing) (flip E.getTeamMember uid) . headMay +getTeamMember :: (Member TeamStore r, Member TeamSubsystem r) => UserId -> Maybe TeamId -> Sem r (Maybe TeamMember) +getTeamMember uid (Just tid) = TeamSubsystem.internalGetTeamMember uid tid +getTeamMember uid Nothing = TeamStore.getUserTeams uid >>= maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember uid) . headMay ---------------------------------------------------------------------------- -- Other kinds of conversations @@ -496,7 +505,8 @@ createOne2OneConversation :: Member Now r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -524,13 +534,13 @@ createOne2OneConversation lusr zcon j = where verifyMembership :: ( Member (ErrorS 'NoBindingTeamMembers) r, - Member TeamStore r + Member TeamSubsystem r ) => TeamId -> UserId -> Sem r () verifyMembership tid u = do - membership <- E.getTeamMember tid u + membership <- TeamSubsystem.internalGetTeamMember u tid when (isNothing membership) $ throwS @'NoBindingTeamMembers checkBindingTeamPermissions :: @@ -540,21 +550,22 @@ createOne2OneConversation lusr zcon j = Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamNotFound) r, Member TeamCollaboratorsSubsystem r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r (Maybe TeamId) checkBindingTeamPermissions lother tid = do mTeamCollaborator <- internalGetTeamCollaborator tid (tUnqualified lusr) - zusrMembership <- E.getTeamMember tid (tUnqualified lusr) + zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid case (mTeamCollaborator, zusrMembership) of (Just collaborator, Nothing) -> guardPerm CollaboratorPermission.ImplicitConnection collaborator (Nothing, mbMember) -> void $ permissionCheck CreateConversation mbMember (Just collaborator, Just member) -> unless (hasPermission collaborator CollaboratorPermission.ImplicitConnection || hasPermission member CreateConversation) $ throwS @OperationDenied - E.getTeamBinding tid >>= \case + TeamStore.getTeamBinding tid >>= \case Just Binding -> do when (isJust zusrMembership) $ verifyMembership tid (tUnqualified lusr) diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index b80a3335035..cb3b2d8b1e7 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -100,6 +100,7 @@ import Wire.Sem.Now qualified as Now import Wire.StoredConversation import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) import Wire.UserList (UserList (UserList)) type FederationAPI = "federation" :> FedApi 'Galley @@ -275,7 +276,7 @@ leaveConversation :: Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamStore r, + Member TeamSubsystem r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r ) => @@ -399,7 +400,7 @@ sendMessage :: Member (Input Opts) r, Member Now r, Member ExternalAccess r, - Member TeamStore r, + Member TeamSubsystem r, Member P.TinyLog r ) => Domain -> @@ -489,7 +490,7 @@ updateConversation :: Member Now r, Member LegalHoldStore r, Member ProposalStore r, - Member TeamStore r, + Member TeamSubsystem r, Member TinyLog r, Member Resource r, Member ConversationStore r, @@ -497,7 +498,8 @@ updateConversation :: Member TeamFeatureStore r, Member (Input (Local ())) r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamStore r ) => Domain -> ConversationUpdateRequest -> @@ -630,6 +632,7 @@ sendMLSCommitBundle :: Member TeamFeatureStore r, Member Resource r, Member TeamStore r, + Member TeamSubsystem r, Member P.TinyLog r, Member Random r, Member ProposalStore r, @@ -686,10 +689,10 @@ sendMLSMessage :: Member (Input Opts) r, Member Now r, Member LegalHoldStore r, - Member TeamStore r, Member P.TinyLog r, Member ProposalStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamStore r ) => Domain -> MLSMessageSendRequest -> @@ -716,7 +719,7 @@ sendMLSMessage remoteDomain msr = handleMLSMessageErrors $ do getSubConversationForRemoteUser :: ( Member ConversationStore r, Member (Input (Local ())) r, - Member TeamStore r + Member TeamSubsystem r ) => Domain -> GetSubConversationsRequest -> @@ -735,7 +738,7 @@ leaveSubConversation :: Member (Error FederationError) r, Member (Input (Local ())) r, Member Resource r, - Member TeamStore r, + Member TeamSubsystem r, Member E.MLSCommitLockStore r ) => Domain -> @@ -757,7 +760,7 @@ deleteSubConversationForRemoteUser :: ( Member ConversationStore r, Member (Input (Local ())) r, Member Resource r, - Member TeamStore r, + Member TeamSubsystem r, Member E.MLSCommitLockStore r ) => Domain -> @@ -972,7 +975,7 @@ updateTypingIndicator :: Member ConversationStore r, Member Now r, Member (Input (Local ())) r, - Member TeamStore r + Member TeamSubsystem r ) => Domain -> TypingDataUpdateRequest -> diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 65f7f12e9de..a60dd23a448 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -56,9 +56,7 @@ import Galley.App import Galley.Effects import Galley.Effects.ClientStore import Galley.Effects.CustomBackendStore -import Galley.Effects.LegalHoldStore as LegalHoldStore -import Galley.Effects.TeamStore -import Galley.Effects.TeamStore qualified as E +import Galley.Env (FanoutLimit) import Galley.Monad import Galley.Options hiding (brig) import Galley.Queue qualified as Q @@ -94,6 +92,7 @@ import Wire.BackendNotificationQueueAccess import Wire.ConversationStore import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.LegalHoldStore as LegalHoldStore import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now @@ -102,6 +101,9 @@ import Wire.Sem.Paging.Cassandra import Wire.ServiceStore import Wire.StoredConversation import Wire.StoredConversation qualified as Data +import Wire.TeamStore +import Wire.TeamStore qualified as E +import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList @@ -212,14 +214,16 @@ iTeamsAPI = mkAPI $ \tid -> hoistAPIHandler Imports.id (base tid) <@> mkNamedAPI @"update-team-status" (Teams.updateTeamStatus tid) <@> hoistAPISegment ( mkNamedAPI @"unchecked-add-team-member" (Teams.uncheckedAddTeamMember tid) - <@> mkNamedAPI @"unchecked-get-team-members" (TeamSubsystem.internalGetTeamMembers tid) + <@> mkNamedAPI @"unchecked-get-team-members" (TeamSubsystem.internalGetTeamMembersWithLimit tid) <@> mkNamedAPI @"unchecked-select-team-member-infos" (\userIds -> TeamSubsystem.internalSelectTeamMemberInfos tid (cUsers userIds)) + <@> mkNamedAPI @"unchecked-select-team-members" (\userIds -> TeamSubsystem.internalSelectTeamMembers tid (cUsers userIds)) <@> mkNamedAPI @"unchecked-get-team-member" (Teams.uncheckedGetTeamMember tid) <@> mkNamedAPI @"can-user-join-team" (Teams.canUserJoinTeam tid) <@> mkNamedAPI @"unchecked-update-team-member" (Teams.uncheckedUpdateTeamMember Nothing Nothing tid) <@> mkNamedAPI @"unchecked-get-team-admins" (TeamSubsystem.internalGetTeamAdmins tid) ) <@> mkNamedAPI @"user-is-team-owner" (Teams.userIsTeamOwner tid) + <@> mkNamedAPI @"finalize-delete-team" (\lusr mconn -> TeamSubsystem.internalFinalizeDeleteTeam lusr mconn tid $> NoContent) <@> hoistAPISegment ( mkNamedAPI @"get-search-visibility-internal" (Teams.getSearchVisibilityInternal tid) <@> mkNamedAPI @"set-search-visibility-internal" (Teams.setSearchVisibilityInternal (featureEnabledForTeam @SearchVisibilityAvailableConfig) tid) @@ -335,7 +339,9 @@ rmUser :: Member P.TinyLog r, Member Random r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -472,7 +478,7 @@ deleteLoop = do doDelete usr con tid = do lusr <- qualifyLocal usr - Teams.uncheckedDeleteTeam lusr con tid + TeamSubsystem.internalFinalizeDeleteTeam lusr con tid safeForever :: String -> App () -> App () safeForever funName action = @@ -484,10 +490,10 @@ safeForever funName action = guardLegalholdPolicyConflictsH :: ( Member BrigAPIAccess r, Member (Input Opts) r, - Member TeamStore r, Member P.TinyLog r, Member (ErrorS 'MissingLegalholdConsent) r, - Member (ErrorS 'MissingLegalholdConsentOldClients) r + Member (ErrorS 'MissingLegalholdConsentOldClients) r, + Member TeamSubsystem r ) => GuardLegalholdPolicyConflicts -> Sem r () diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 600784f8027..70cddd73bfb 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -31,7 +31,6 @@ module Galley.API.LegalHold where import Brig.Types.Connection (UpdateConnectionsInternal (..)) -import Brig.Types.Team.LegalHold (legalHoldService, viewLegalHoldService) import Control.Exception (assert) import Control.Lens (view, (^.)) import Data.ByteString.Conversion (toByteString) @@ -50,9 +49,7 @@ import Galley.API.Update (removeMemberFromLocalConv) import Galley.API.Util import Galley.App import Galley.Effects -import Galley.Effects.LegalHoldStore qualified as LegalHoldData import Galley.Effects.TeamMemberStore -import Galley.Effects.TeamStore import Galley.External.LegalHoldService qualified as LHService import Galley.Types.Teams as Team import Imports @@ -71,15 +68,18 @@ import Wire.API.Federation.Error import Wire.API.Provider.Service import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Public.Galley.LegalHold +import Wire.API.Team.Feature (LegalholdConfig) import Wire.API.Team.LegalHold import Wire.API.Team.LegalHold qualified as Public import Wire.API.Team.LegalHold.External hiding (userId) +import Wire.API.Team.LegalHold.Internal import Wire.API.Team.Member import Wire.API.User.Client.Prekey import Wire.BrigAPIAccess import Wire.ConversationStore import Wire.ConversationSubsystem import Wire.FireAndForget +import Wire.LegalHoldStore qualified as LegalHoldData import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Paging @@ -87,6 +87,9 @@ import Wire.Sem.Paging.Cassandra import Wire.StoredConversation import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem createSettings :: forall r. @@ -97,8 +100,9 @@ createSettings :: Member (ErrorS 'LegalHoldServiceBadResponse) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -107,7 +111,7 @@ createSettings :: createSettings lzusr tid newService = do let zusr = tUnqualified lzusr assertLegalHoldEnabledForTeam tid - zusrMembership <- getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid -- let zothers = map (view userId) membs -- Log.debug $ -- Log.field "targets" (toByteString . show $ toByteString <$> zothers) @@ -126,14 +130,15 @@ getSettings :: ( Member (ErrorS 'NotATeamMember) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r Public.ViewLegalHoldService getSettings lzusr tid = do let zusr = tUnqualified lzusr - zusrMembership <- getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid void $ maybe (throwS @'NotATeamMember) pure zusrMembership isenabled <- isLegalHoldEnabledForTeam tid mresult <- LegalHoldData.getSettings tid @@ -175,7 +180,9 @@ removeSettingsInternalPaging :: Member TeamStore r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -218,7 +225,9 @@ removeSettings :: Member Random r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => UserId -> TeamId -> @@ -227,7 +236,7 @@ removeSettings :: removeSettings zusr tid (Public.RemoveLegalHoldSettingsRequest mPassword) = do assertNotWhitelisting assertLegalHoldEnabledForTeam tid - zusrMembership <- getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid -- let zothers = map (view userId) membs -- Log.debug $ -- Log.field "targets" (toByteString . show $ toByteString <$> zothers) @@ -238,7 +247,8 @@ removeSettings zusr tid (Public.RemoveLegalHoldSettingsRequest mPassword) = do where assertNotWhitelisting :: Sem r () assertNotWhitelisting = do - getLegalHoldFlag >>= \case + featureLegalHold <- input @(FeatureDefaults LegalholdConfig) + case featureLegalHold of FeatureLegalHoldDisabledPermanently -> pure () FeatureLegalHoldDisabledByDefault -> pure () FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> @@ -274,7 +284,8 @@ removeSettings' :: Member P.TinyLog r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => TeamId -> Sem r () @@ -323,7 +334,8 @@ grantConsent :: Member Random r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -331,7 +343,7 @@ grantConsent :: grantConsent lusr tid = do userLHStatus <- noteS @'TeamMemberNotFound - =<< fmap (view legalHoldStatus) <$> getTeamMember tid (tUnqualified lusr) + =<< fmap (view legalHoldStatus) <$> TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid case userLHStatus of lhs@UserLegalHoldNoConsent -> changeLegalholdStatusAndHandlePolicyConflicts tid lusr lhs UserLegalHoldDisabled $> GrantConsentSuccess @@ -374,7 +386,9 @@ requestDevice :: Member TeamStore r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -387,9 +401,9 @@ requestDevice lzusr tid uid = do P.debug $ Log.field "targets" (toByteString (tUnqualified luid)) . Log.field "action" (Log.val "LegalHold.requestDevice") - zusrMembership <- getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid void $ permissionCheck ChangeLegalHoldUserSettings zusrMembership - member <- noteS @'TeamMemberNotFound =<< getTeamMember tid uid + member <- noteS @'TeamMemberNotFound =<< TeamSubsystem.internalGetTeamMember uid tid case member ^. legalHoldStatus of UserLegalHoldEnabled -> throwS @'UserLegalHoldAlreadyEnabled lhs@UserLegalHoldPending -> @@ -468,7 +482,9 @@ approveDevice :: Member TeamStore r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -487,7 +503,7 @@ approveDevice lzusr connId tid uid (Public.ApproveLegalHoldForUserRequest mPassw assertOnTeam (tUnqualified luid) tid ensureReAuthorised zusr mPassword Nothing Nothing userLHStatus <- - maybe defUserLegalHoldStatus (view legalHoldStatus) <$> getTeamMember tid (tUnqualified luid) + maybe defUserLegalHoldStatus (view legalHoldStatus) <$> TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid assertUserLHPending userLHStatus mPreKeys <- LegalHoldData.selectPendingPrekeys (tUnqualified luid) (prekeys, lastPrekey') <- case mPreKeys of @@ -545,7 +561,8 @@ disableForUser :: Member TeamStore r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -557,11 +574,11 @@ disableForUser lzusr tid uid (Public.DisableLegalHoldForUserRequest mPassword) = P.debug $ Log.field "targets" (toByteString (tUnqualified luid)) . Log.field "action" (Log.val "LegalHold.disableForUser") - zusrMembership <- getTeamMember tid (tUnqualified lzusr) + zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified lzusr) tid void $ permissionCheck ChangeLegalHoldUserSettings zusrMembership userLHStatus <- - maybe defUserLegalHoldStatus (view legalHoldStatus) <$> getTeamMember tid (tUnqualified luid) + maybe defUserLegalHoldStatus (view legalHoldStatus) <$> TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid let doDisable = disableLH (tUnqualified lzusr) luid userLHStatus $> DisableLegalHoldSuccess case userLHStatus of @@ -609,7 +626,8 @@ changeLegalholdStatusAndHandlePolicyConflicts :: Member Random r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => TeamId -> Local UserId -> @@ -656,7 +674,8 @@ blockNonConsentingConnections :: ( Member BrigAPIAccess r, Member TeamStore r, Member P.TinyLog r, - Member (ErrorS 'LegalHoldCouldNotBlockConnections) r + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member TeamSubsystem r ) => UserId -> Sem r () @@ -725,7 +744,8 @@ handleGroupConvPolicyConflicts :: Member Random r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> UserLegalHoldStatus -> diff --git a/services/galley/src/Galley/API/LegalHold/Conflicts.hs b/services/galley/src/Galley/API/LegalHold/Conflicts.hs index ca3f63920b8..a073f055f29 100644 --- a/services/galley/src/Galley/API/LegalHold/Conflicts.hs +++ b/services/galley/src/Galley/API/LegalHold/Conflicts.hs @@ -1,19 +1,3 @@ --- 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 . {-# OPTIONS_GHC -Wno-overlapping-patterns #-} -- This file is part of the Wire Server implementation. @@ -51,7 +35,6 @@ import Data.Qualified import Data.Set qualified as Set import Galley.API.Util import Galley.Effects -import Galley.Effects.TeamStore import Galley.Options import Galley.Types.Teams import Imports @@ -66,6 +49,8 @@ import Wire.API.Team.Member import Wire.API.User import Wire.API.User.Client as Client import Wire.BrigAPIAccess +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem data LegalholdConflicts = LegalholdConflicts @@ -76,8 +61,8 @@ guardQualifiedLegalholdPolicyConflicts :: Member (Error LegalholdConflicts) r, Member (Input (Local ())) r, Member (Input Opts) r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => LegalholdProtectee -> QualifiedUserClients -> @@ -100,8 +85,8 @@ guardLegalholdPolicyConflicts :: ( Member BrigAPIAccess r, Member (Error LegalholdConflicts) r, Member (Input Opts) r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => LegalholdProtectee -> UserClients -> @@ -124,8 +109,8 @@ guardLegalholdPolicyConflictsUid :: forall r. ( Member BrigAPIAccess r, Member (Error LegalholdConflicts) r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => UserId -> UserClients -> @@ -152,7 +137,7 @@ guardLegalholdPolicyConflictsUid self (Map.keys . userClients -> otherUids) = do checkUserConsentMissing user = case userTeam user of Just tid -> do - mbMem <- getTeamMember tid (Wire.API.User.userId user) + mbMem <- TeamSubsystem.internalGetTeamMember (Wire.API.User.userId user) tid case mbMem of Nothing -> pure True -- it's weird that there is a member id but no member, we better bail Just mem -> pure $ case mem ^. legalHoldStatus of diff --git a/services/galley/src/Galley/API/LegalHold/Get.hs b/services/galley/src/Galley/API/LegalHold/Get.hs index 3607c040060..e6ac3379fac 100644 --- a/services/galley/src/Galley/API/LegalHold/Get.hs +++ b/services/galley/src/Galley/API/LegalHold/Get.hs @@ -24,8 +24,6 @@ import Data.LegalHold (UserLegalHoldStatus (..)) import Data.Qualified import Galley.API.Error import Galley.Effects -import Galley.Effects.LegalHoldStore qualified as LegalHoldData -import Galley.Effects.TeamStore import Imports import Polysemy import Polysemy.Error @@ -37,6 +35,9 @@ import Wire.API.Team.LegalHold import Wire.API.Team.LegalHold qualified as Public import Wire.API.Team.Member import Wire.API.User.Client.Prekey +import Wire.LegalHoldStore qualified as LegalHoldData +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem -- | Learn whether a user has LH enabled and fetch pre-keys. -- Note that this is accessible to ANY authenticated user, even ones outside the team @@ -45,15 +46,15 @@ getUserStatus :: ( Member (Error InternalError) r, Member (ErrorS 'TeamMemberNotFound) r, Member LegalHoldStore r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> UserId -> Sem r Public.UserLegalHoldStatusResponse getUserStatus _lzusr tid uid = do - teamMember <- noteS @'TeamMemberNotFound =<< getTeamMember tid uid + teamMember <- noteS @'TeamMemberNotFound =<< TeamSubsystem.internalGetTeamMember uid tid let status = view legalHoldStatus teamMember (mlk, lcid) <- case status of UserLegalHoldNoConsent -> pure (Nothing, Nothing) diff --git a/services/galley/src/Galley/API/LegalHold/Team.hs b/services/galley/src/Galley/API/LegalHold/Team.hs index ae5460aeb13..557b19fee5b 100644 --- a/services/galley/src/Galley/API/LegalHold/Team.hs +++ b/services/galley/src/Galley/API/LegalHold/Team.hs @@ -28,23 +28,24 @@ import Data.Default import Data.Id import Data.Range import Galley.Effects -import Galley.Effects.LegalHoldStore qualified as LegalHoldData import Galley.Effects.TeamFeatureStore -import Galley.Effects.TeamStore +import Galley.Env import Galley.Types.Teams as Team import Imports import Polysemy +import Polysemy.Input (Input, input) import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Team.Feature import Wire.API.Team.Size import Wire.BrigAPIAccess +import Wire.LegalHoldStore qualified as LegalHoldData assertLegalHoldEnabledForTeam :: forall r. ( Member LegalHoldStore r, - Member TeamStore r, Member TeamFeatureStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, Member (ErrorS 'LegalHoldNotEnabled) r ) => TeamId -> @@ -54,14 +55,15 @@ assertLegalHoldEnabledForTeam tid = throwS @'LegalHoldNotEnabled computeLegalHoldFeatureStatus :: - ( Member TeamStore r, - Member LegalHoldStore r + ( Member LegalHoldStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> DbFeature LegalholdConfig -> Sem r FeatureStatus -computeLegalHoldFeatureStatus tid dbFeature = - getLegalHoldFlag >>= \case +computeLegalHoldFeatureStatus tid dbFeature = do + featureLegalHold <- input @(FeatureDefaults LegalholdConfig) + case featureLegalHold of FeatureLegalHoldDisabledPermanently -> pure FeatureStatusDisabled FeatureLegalHoldDisabledByDefault -> pure (applyDbFeature dbFeature def).status @@ -72,8 +74,8 @@ computeLegalHoldFeatureStatus tid dbFeature = isLegalHoldEnabledForTeam :: forall r. ( Member LegalHoldStore r, - Member TeamStore r, - Member TeamFeatureStore r + Member TeamFeatureStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Sem r Bool @@ -85,7 +87,8 @@ isLegalHoldEnabledForTeam tid = do ensureNotTooLargeToActivateLegalHold :: ( Member BrigAPIAccess r, Member (ErrorS 'CannotEnableLegalHoldServiceLargeTeam) r, - Member TeamStore r + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Sem r () @@ -94,11 +97,17 @@ ensureNotTooLargeToActivateLegalHold tid = do unlessM (teamSizeBelowLimit (fromIntegral teamSize)) $ throwS @'CannotEnableLegalHoldServiceLargeTeam -teamSizeBelowLimit :: (Member TeamStore r) => Int -> Sem r Bool +teamSizeBelowLimit :: + ( Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r + ) => + Int -> + Sem r Bool teamSizeBelowLimit teamSize = do - limit <- fromIntegral . fromRange <$> fanoutLimit + limit <- fromIntegral . fromRange <$> input @FanoutLimit let withinLimit = teamSize <= limit - getLegalHoldFlag >>= \case + featureLegalHold <- input @(FeatureDefaults LegalholdConfig) + case featureLegalHold of FeatureLegalHoldDisabledPermanently -> pure withinLimit FeatureLegalHoldDisabledByDefault -> pure withinLimit FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index a89eacd3df6..454bca113b5 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -62,6 +62,7 @@ import Wire.ConversationStore import Wire.ConversationStore.MLS.Types import Wire.ConversationSubsystem import Wire.StoredConversation +import Wire.TeamSubsystem (TeamSubsystem) processInternalCommit :: forall r. @@ -77,7 +78,8 @@ processInternalCommit :: Member Resource r, Member Random r, Member (ErrorS MLSInvalidLeafNodeSignature) r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => SenderIdentity -> Maybe ConnId -> @@ -256,7 +258,7 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdat pure events addMembers :: - (HasProposalActionEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r) => + (HasProposalActionEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r, Member TeamSubsystem r) => Qualified UserId -> Maybe ConnId -> Local ConvOrSubConv -> @@ -280,7 +282,7 @@ addMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of SubConv _ _ -> pure [] removeMembers :: - (HasProposalActionEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r) => + (HasProposalActionEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r, Member TeamSubsystem r) => Qualified UserId -> Maybe ConnId -> Local ConvOrSubConv -> diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 154b651b473..75abbecc6a2 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -59,7 +59,6 @@ import Galley.API.MLS.Welcome (sendWelcomes) import Galley.API.Util import Galley.Effects import Galley.Effects.FederatorAccess -import Galley.Effects.TeamStore qualified as TeamStore import Imports import Polysemy import Polysemy.Error @@ -92,6 +91,8 @@ import Wire.ConversationSubsystem import Wire.NotificationSubsystem import Wire.Sem.Now qualified as Now import Wire.StoredConversation +import Wire.TeamStore qualified as TeamStore +import Wire.TeamSubsystem (TeamSubsystem) -- FUTUREWORK -- - Check that the capabilities of a leaf node in an add proposal contains all @@ -181,7 +182,8 @@ postMLSCommitBundle :: Members MLSBundleStaticErrors r, HasProposalEffects r, Member ConversationSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => Local x -> Qualified UserId -> @@ -210,7 +212,8 @@ postMLSCommitBundleFromLocalUser :: Members MLSBundleStaticErrors r, HasProposalEffects r, Member ConversationSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => Version -> Local UserId -> @@ -243,7 +246,8 @@ postMLSCommitBundleToLocalConv :: Members MLSBundleStaticErrors r, HasProposalEffects r, Member ConversationSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => Qualified UserId -> ClientId -> diff --git a/services/galley/src/Galley/API/MLS/Reset.hs b/services/galley/src/Galley/API/MLS/Reset.hs index a1b2addba73..c47dc8f8cda 100644 --- a/services/galley/src/Galley/API/MLS/Reset.hs +++ b/services/galley/src/Galley/API/MLS/Reset.hs @@ -43,6 +43,7 @@ import Wire.ConversationSubsystem import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) resetMLSConversation :: ( Member (Input Env) r, @@ -68,10 +69,10 @@ resetMLSConversation :: Member ProposalStore r, Member Random r, Member Resource r, - Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> MLSReset -> diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index b7a452c2c40..3badd394185 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -70,6 +70,7 @@ import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.StoredConversation import Wire.StoredConversation qualified as Data +import Wire.TeamSubsystem (TeamSubsystem) type MLSGetSubConvStaticErrors = '[ ErrorS 'ConvNotFound, @@ -84,7 +85,7 @@ getSubConversation :: Member (ErrorS 'MLSSubConvUnsupportedConvType) r, Member (Error FederationError) r, Member FederatorAccess r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> Qualified ConvId -> @@ -102,7 +103,7 @@ getLocalSubConversation :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'MLSSubConvUnsupportedConvType) r, - Member TeamStore r + Member TeamSubsystem r ) => Qualified UserId -> Local ConvId -> @@ -209,8 +210,8 @@ deleteSubConversation :: Member FederatorAccess r, Member (Input Env) r, Member Resource r, - Member TeamStore r, - Member Eff.MLSCommitLockStore r + Member Eff.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> Qualified ConvId -> @@ -283,8 +284,8 @@ leaveSubConversation :: Member (ErrorS 'MLSNotEnabled) r, Member Resource r, Members LeaveSubConversationStaticErrors r, - Member TeamStore r, - Member Eff.MLSCommitLockStore r + Member Eff.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ClientId -> @@ -309,8 +310,8 @@ leaveLocalSubConversation :: Member (Error FederationError) r, Member Resource r, Members LeaveSubConversationStaticErrors r, - Member TeamStore r, - Member Eff.MLSCommitLockStore r + Member Eff.MLSCommitLockStore r, + Member TeamSubsystem r ) => ClientIdentity -> Local ConvId -> @@ -382,8 +383,8 @@ resetLocalSubConversation :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'MLSStaleMessage) r, Member Resource r, - Member TeamStore r, - Member Eff.MLSCommitLockStore r + Member Eff.MLSCommitLockStore r, + Member TeamSubsystem r ) => Qualified UserId -> Local ConvId -> diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 2ecd0c65513..fcf2bf5e511 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -55,7 +55,7 @@ import Galley.API.Util import Galley.Effects import Galley.Effects.ClientStore import Galley.Effects.FederatorAccess -import Galley.Effects.TeamStore +import Galley.Env import Galley.Options import Galley.Types.Clients qualified as Clients import Imports hiding (forkIO) @@ -87,6 +87,9 @@ import Wire.NotificationSubsystem (NotificationSubsystem) import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation +import Wire.TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem data UserType = User | Bot @@ -259,7 +262,9 @@ postBroadcast :: Member Now r, Member TeamStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -278,7 +283,7 @@ postBroadcast lusr con msg = runError $ do now <- Now.get tid <- lookupBindingTeam senderUser - limit <- fromIntegral . fromRange <$> fanoutLimit + limit <- fromIntegral . fromRange <$> input @FanoutLimit -- If we are going to fan this out to more than limit, we want to fail early unless (Map.size rcps <= limit) $ throwS @'BroadcastLimitExceeded @@ -331,7 +336,7 @@ postBroadcast lusr con msg = runError $ do where maybeFetchLimitedTeamMemberList :: ( Member (ErrorS 'BroadcastLimitExceeded) r, - Member TeamStore r + Member TeamSubsystem r ) => Int -> TeamId -> @@ -343,11 +348,12 @@ postBroadcast lusr con msg = runError $ do let localUserIdsToLookup = Set.toList $ Set.union (Set.fromList localUserIdsInFilter) (Set.fromList localUserIdsInRcps) unless (length localUserIdsToLookup <= limit) $ throwS @'BroadcastLimitExceeded - selectTeamMembers tid localUserIdsToLookup + TeamSubsystem.internalSelectTeamMembers tid localUserIdsToLookup maybeFetchAllMembersInTeam :: ( Member (ErrorS 'BroadcastLimitExceeded) r, - Member TeamStore r + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> Sem r [TeamMember] @@ -366,9 +372,9 @@ postQualifiedOtrMessage :: Member ExternalAccess r, Member (Input Opts) r, Member Now r, - Member TeamStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member TeamSubsystem r ) => UserType -> Qualified UserId -> @@ -526,8 +532,8 @@ guardQualifiedLegalholdPolicyConflictsWrapper :: ( Member BrigAPIAccess r, Member (Error (MessageNotSent MessageSendingStatus)) r, Member (Input Opts) r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => UserType -> Qualified UserId -> diff --git a/services/galley/src/Galley/API/Public/Bot.hs b/services/galley/src/Galley/API/Public/Bot.hs index ad839a6f24f..4d151325496 100644 --- a/services/galley/src/Galley/API/Public/Bot.hs +++ b/services/galley/src/Galley/API/Public/Bot.hs @@ -34,6 +34,7 @@ import Wire.API.Event.Team qualified as Public () import Wire.API.Provider.Bot import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot +import Wire.TeamSubsystem (TeamSubsystem) botAPI :: API BotAPI GalleyEffects botAPI = @@ -48,7 +49,8 @@ getBotConversation :: Member TeamFeatureStore r, Member (ErrorS 'AccessDenied) r, Member (ErrorS 'ConvNotFound) r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => BotId -> ConvId -> diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index d70c47254b6..2bbd130146a 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -79,7 +79,6 @@ import Galley.Data.Types (Code (codeConversation)) import Galley.Data.Types qualified as Data import Galley.Effects import Galley.Effects.FederatorAccess qualified as E -import Galley.Effects.TeamStore qualified as E import Galley.Env import Galley.Options import Imports @@ -118,13 +117,15 @@ import Wire.Sem.Paging.Cassandra import Wire.StoredConversation import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList getBotConversation :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (Input (Local ())) r, - Member TeamStore r + Member TeamSubsystem r ) => BotId -> ConvId -> @@ -151,7 +152,7 @@ getUnqualifiedOwnConversation :: Member (ErrorS 'ConvAccessDenied) r, Member (Error InternalError) r, Member P.TinyLog r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConvId -> @@ -165,7 +166,7 @@ getUnqualifiedConversation :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConvId -> @@ -182,7 +183,7 @@ getConversation :: Member (Error FederationError) r, Member FederatorAccess r, Member P.TinyLog r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> Qualified ConvId -> @@ -203,7 +204,7 @@ getOwnConversation :: Member (Error InternalError) r, Member FederatorAccess r, Member P.TinyLog r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> Qualified ConvId -> @@ -361,7 +362,7 @@ getConversationRoles :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConvId -> @@ -640,11 +641,11 @@ getConversationByReusableCode :: Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'GuestLinksDisabled) r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r, Member TeamFeatureStore r, Member (Input Opts) r, Member HashPassword r, - Member RateLimit r + Member RateLimit r, + Member TeamSubsystem r ) => Local UserId -> Key -> @@ -685,14 +686,14 @@ getConversationGuestLinksStatus :: Member (ErrorS 'ConvAccessDenied) r, Member (Input Opts) r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> ConvId -> Sem r (LockableFeature GuestLinksConfig) getConversationGuestLinksStatus uid convId = do conv <- E.getConversation convId >>= noteS @'ConvNotFound - mTeamMember <- maybe (pure Nothing) (flip E.getTeamMember uid) conv.metadata.cnvmTeam + mTeamMember <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember uid) conv.metadata.cnvmTeam ensureConvAdmin conv uid mTeamMember getConversationGuestLinksFeatureStatus (Data.convTeam conv) @@ -780,7 +781,8 @@ getMLSOne2OneConversationV5 :: Member FederatorAccess r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> Qualified UserId -> @@ -801,7 +803,8 @@ getMLSOne2OneConversationInternal :: Member FederatorAccess r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> Qualified UserId -> @@ -820,7 +823,8 @@ getMLSOne2OneConversationV6 :: Member FederatorAccess r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> Qualified UserId -> @@ -846,7 +850,8 @@ getMLSOne2OneConversation :: Member FederatorAccess r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> Qualified UserId -> @@ -994,7 +999,7 @@ searchChannels :: ( Member ConversationStore r, Member (ErrorS NotATeamMember) r, Member (ErrorS OperationDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -1007,7 +1012,7 @@ searchChannels :: Sem r ConversationPage searchChannels lusr tid searchString sortOrder pageSize lastName lastId discoverable = do r <- runError @(Tagged OperationDenied ()) $ do - mem <- E.getTeamMember tid (tUnqualified lusr) + mem <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid void $ permissionCheck SearchChannels mem case r of Left e | not discoverable -> throw e diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 314ae7b43c5..0324fe1a16e 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1,19 +1,3 @@ --- 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 . {-# LANGUAGE LambdaCase #-} -- This file is part of the Wire Server implementation. @@ -44,7 +28,6 @@ module Galley.API.Teams getBindingTeamMembers, getManyTeams, deleteTeam, - uncheckedDeleteTeam, addTeamMember, getTeamConversationRoles, getTeamMembers, @@ -86,8 +69,6 @@ import Data.HashMap.Strict qualified as HM import Data.Id import Data.Json.Util import Data.LegalHold qualified as LH -import Data.List.Extra qualified as List -import Data.List.NonEmpty (NonEmpty ((:|))) import Data.Map qualified as Map import Data.Proxy import Data.Qualified @@ -104,13 +85,10 @@ import Galley.API.Update qualified as API import Galley.API.Util import Galley.App import Galley.Effects -import Galley.Effects.LegalHoldStore qualified as Data import Galley.Effects.Queue qualified as E import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData -import Galley.Effects.SparAccess qualified as Spar import Galley.Effects.TeamMemberStore qualified as E -import Galley.Effects.TeamStore qualified as E -import Galley.Intra.Journal qualified as Journal +import Galley.Env import Galley.Options import Galley.Types.Teams import Imports hiding (forkIO) @@ -125,13 +103,12 @@ import Wire.API.Conversation.Role (wireConvRoles) import Wire.API.Conversation.Role qualified as Public import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Event.Conversation qualified as Conv import Wire.API.Event.LeaveReason import Wire.API.Event.Team import Wire.API.Federation.Error import Wire.API.Push.V2 (RecipientClients (RecipientClientsAll)) import Wire.API.Routes.Internal.Galley.TeamsIntra -import Wire.API.Routes.MultiTablePaging (MultiTablePage (..), MultiTablePagingState (mtpsState)) +import Wire.API.Routes.MultiTablePaging (MultiTablePage (..), MultiTablePagingState (..)) import Wire.API.Routes.Public.Galley.TeamMember import Wire.API.Team import Wire.API.Team qualified as Public @@ -152,7 +129,6 @@ import Wire.BrigAPIAccess qualified as Brig import Wire.BrigAPIAccess qualified as E import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem -import Wire.ExternalAccess qualified as E import Wire.ListItems qualified as E import Wire.NotificationSubsystem import Wire.Sem.Now @@ -160,13 +136,16 @@ import Wire.Sem.Now qualified as Now import Wire.Sem.Paging.Cassandra import Wire.StoredConversation import Wire.TeamCollaboratorsSubsystem +import Wire.TeamJournal (TeamJournal) +import Wire.TeamJournal qualified as Journal +import Wire.TeamStore qualified as E import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList getTeamH :: forall r. - (Member (ErrorS 'TeamNotFound) r, Member (Queue DeleteItem) r, Member TeamStore r) => + (Member (ErrorS 'TeamNotFound) r, Member (Queue DeleteItem) r, Member TeamStore r, Member TeamSubsystem r) => UserId -> TeamId -> Sem r Public.Team @@ -210,7 +189,8 @@ getTeamNameInternal = fmap (fmap TeamName) . E.getTeamName getManyTeams :: ( Member TeamStore r, Member (Queue DeleteItem) r, - Member (ListItems LegacyPaging TeamId) r + Member (ListItems LegacyPaging TeamId) r, + Member TeamSubsystem r ) => UserId -> Sem r Public.TeamList @@ -221,13 +201,14 @@ getManyTeams zusr = lookupTeam :: ( Member TeamStore r, - Member (Queue DeleteItem) r + Member (Queue DeleteItem) r, + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r (Maybe Public.Team) lookupTeam zusr tid = do - tm <- E.getTeamMember tid zusr + tm <- TeamSubsystem.internalGetTeamMember zusr tid if isJust tm then do t <- E.getTeam tid @@ -277,7 +258,8 @@ updateTeamStatus :: Member (ErrorS 'InvalidTeamStatusUpdate) r, Member (ErrorS 'TeamNotFound) r, Member Now r, - Member TeamStore r + Member TeamStore r, + Member TeamJournal r ) => TeamId -> TeamStatusUpdate -> @@ -316,7 +298,8 @@ updateTeamH :: Member (ErrorS ('MissingPermission ('Just 'SetTeamData))) r, Member NotificationSubsystem r, Member Now r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => UserId -> ConnId -> @@ -324,7 +307,7 @@ updateTeamH :: Public.TeamUpdateData -> Sem r () updateTeamH zusr zcon tid updateData = do - zusrMembership <- E.getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid void $ permissionCheckS SSetTeamData zusrMembership E.setTeamData tid updateData now <- Now.get @@ -350,7 +333,8 @@ deleteTeam :: Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamNotFound) r, Member (Queue DeleteItem) r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => UserId -> ConnId -> @@ -368,7 +352,7 @@ deleteTeam zusr zcon tid body = do queueTeamDeletion tid zusr (Just zcon) where checkPermissions team = do - void $ permissionCheck DeleteTeam =<< E.getTeamMember tid zusr + void $ permissionCheck DeleteTeam =<< TeamSubsystem.internalGetTeamMember zusr tid when (tdTeam team ^. teamBinding == Binding) $ do ensureReAuthorised zusr (body ^. tdAuthPassword) (body ^. tdVerificationCode) (Just U.DeleteTeam) @@ -379,7 +363,8 @@ internalDeleteBindingTeam :: Member (ErrorS 'NotAOneMemberTeam) r, Member (ErrorS 'DeleteQueueFull) r, Member (Queue DeleteItem) r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => TeamId -> Bool -> @@ -390,112 +375,32 @@ internalDeleteBindingTeam tid force = do Nothing -> throwS @'TeamNotFound Just team | team ^. teamBinding /= Binding -> throwS @'NoBindingTeam Just team -> do - mems <- E.getTeamMembersWithLimit tid (unsafeRange 2) + mems <- TeamSubsystem.internalGetTeamMembersWithLimit tid (Just (unsafeRange 2)) case mems ^. teamMembers of [mem] -> queueTeamDeletion tid (mem ^. userId) Nothing -- if the team has more than one member (and deletion is forced) or no members we use the team creator's userId for deletion events xs | null xs || force -> queueTeamDeletion tid (team ^. teamCreator) Nothing _ -> throwS @'NotAOneMemberTeam --- This function is "unchecked" because it does not validate that the user has the `DeleteTeam` permission. -uncheckedDeleteTeam :: - forall r. - ( Member BrigAPIAccess r, - Member ExternalAccess r, - Member NotificationSubsystem r, - Member (Input Opts) r, - Member Now r, - Member LegalHoldStore r, - Member SparAccess r, - Member TeamStore r, - Member ConversationStore r - ) => - Local UserId -> - Maybe ConnId -> - TeamId -> - Sem r () -uncheckedDeleteTeam lusr zcon tid = do - team <- E.getTeam tid - when (isJust team) $ do - Spar.deleteTeam tid - now <- Now.get - convs <- E.getTeamConversations tid - -- Even for LARGE TEAMS, we _DO_ want to fetch all team members here because we - -- want to generate conversation deletion events for non-team users. This should - -- be fine as it is done once during the life team of a team and we still do not - -- fanout this particular event to all team members anyway. And this is anyway - -- done asynchronously - membs <- E.getTeamMembers tid - (ue, be) <- foldrM (createConvDeleteEvents now membs) ([], []) convs - let e = newEvent tid now EdTeamDelete - pushDeleteEvents membs e ue - E.deliverAsync be - -- TODO: we don't delete bots here, but we should do that, since - -- every bot user can only be in a single conversation. Just - -- deleting conversations from the database is not enough. - when ((view teamBinding . tdTeam <$> team) == Just Binding) $ do - mapM_ (E.deleteUser . view userId) membs - Journal.teamDelete tid - Data.unsetTeamLegalholdWhitelisted tid - E.deleteTeam tid - where - pushDeleteEvents :: [TeamMember] -> Event -> [Push] -> Sem r () - pushDeleteEvents membs e ue = do - o <- inputs (view settings) - let r = userRecipient (tUnqualified lusr) :| membersToRecipients (Just (tUnqualified lusr)) membs - -- To avoid DoS on gundeck, send team deletion events in chunks - let chunkSize = fromMaybe defConcurrentDeletionEvents (o ^. concurrentDeletionEvents) - let chunks = List.chunksOf chunkSize (toList r) - forM_ chunks $ \chunk -> - -- push TeamDelete events. Note that despite having a complete list, we are guaranteed in the - -- push module to never fan this out to more than the limit - pushNotifications [def {origin = Just (tUnqualified lusr), json = toJSONObject e, recipients = chunk, conn = zcon}] - -- To avoid DoS on gundeck, send conversation deletion events slowly - pushNotificationsSlowly ue - createConvDeleteEvents :: - UTCTime -> - [TeamMember] -> - ConvId -> - ([Push], [(BotMember, Conv.Event)]) -> - Sem r ([Push], [(BotMember, Conv.Event)]) - createConvDeleteEvents now teamMembs cid (pp, ee) = do - let qconvId = tUntagged $ qualifyAs lusr cid - (bots, convMembs) <- localBotsAndUsers <$> E.getLocalMembers cid - -- Only nonTeamMembers need to get any events, since on team deletion, - -- all team users are deleted immediately after these events are sent - -- and will thus never be able to see these events in practice. - let mm = nonTeamMembers convMembs teamMembs - let e = Conv.Event qconvId Nothing (Conv.EventFromUser (tUntagged lusr)) now (Just tid) Conv.EdConvDelete - -- This event always contains all the required recipients - let p = - def - { origin = Just (tUnqualified lusr), - json = toJSONObject e, - recipients = map localMemberToRecipient mm - } - let ee' = map (,e) bots - let pp' = (p {conn = zcon}) : pp - pure (pp', ee' ++ ee) - getTeamConversationRoles :: ( Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r Public.ConversationRolesList getTeamConversationRoles zusr tid = do - void $ E.getTeamMember tid zusr >>= noteS @'NotATeamMember + void $ TeamSubsystem.internalGetTeamMember zusr tid >>= noteS @'NotATeamMember -- NOTE: If/when custom roles are added, these roles should -- be merged with the team roles (if they exist) pure $ Public.ConversationRolesList wireConvRoles getTeamMembers :: ( Member (ErrorS 'NotATeamMember) r, - Member TeamStore r, Member BrigAPIAccess r, Member (TeamMemberStore CassandraPaging) r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -504,7 +409,7 @@ getTeamMembers :: Sem r TeamMembersPage getTeamMembers lzusr tid mbMaxResults mbPagingState = do let uid = tUnqualified lzusr - member <- E.getTeamMember tid uid >>= noteS @'NotATeamMember + member <- TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'NotATeamMember let mState = C.PagingState . LBS.fromStrict <$> (mbPagingState >>= mtpsState) let mLimit = fromMaybe (unsafeRange Public.hardTruncationLimit) mbMaxResults if member `hasPermission` SearchContacts @@ -537,22 +442,27 @@ getTeamMembers lzusr tid mbMaxResults mbPagingState = do -- we only return the person who invited them and the self user. let invitee = member ^. invitation <&> fst let uids = uid : maybeToList invitee - E.selectTeamMembersPaginated tid uids mState mLimit <&> toTeamMembersPage member + TeamSubsystem.internalSelectTeamMembers tid uids <&> toTeamSingleMembersPage member where toTeamMembersPage :: TeamMember -> C.PageWithState TeamMember -> TeamMembersPage toTeamMembersPage member p = let withPerms = (member `canSeePermsOf`) in TeamMembersPage $ MultiTablePage - (map (setOptionalPerms withPerms) $ pwsResults p) - (pwsHasMore p) - (teamMemberPagingState p) + { mtpResults = map (setOptionalPerms withPerms) $ pwsResults p, + mtpHasMore = pwsHasMore p, + mtpPagingState = teamMemberPagingState p + } + + toTeamSingleMembersPage :: TeamMember -> [TeamMember] -> TeamMembersPage + toTeamSingleMembersPage member = + mkSingleTeamMembersPage . map (setOptionalPerms (member `canSeePermsOf`)) -- | like 'getTeamMembers', but with an explicit list of users we are to return. bulkGetTeamMembers :: ( Member (ErrorS 'BulkGetMemberLimitExceeded) r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -562,8 +472,8 @@ bulkGetTeamMembers :: bulkGetTeamMembers lzusr tid mbMaxResults uids = do unless (length (U.mUsers uids) <= fromIntegral (fromRange (fromMaybe (unsafeRange Public.hardTruncationLimit) mbMaxResults))) $ throwS @'BulkGetMemberLimitExceeded - m <- E.getTeamMember tid (tUnqualified lzusr) >>= noteS @'NotATeamMember - mems <- E.selectTeamMembers tid (U.mUsers uids) + m <- TeamSubsystem.internalGetTeamMember (tUnqualified lzusr) tid >>= noteS @'NotATeamMember + mems <- TeamSubsystem.internalSelectTeamMembers tid (U.mUsers uids) let withPerms = (m `canSeePermsOf`) hasMore = ListComplete pure $ setOptionalPermsMany withPerms (newTeamMemberList mems hasMore) @@ -571,7 +481,7 @@ bulkGetTeamMembers lzusr tid mbMaxResults uids = do getTeamMember :: ( Member (ErrorS 'TeamMemberNotFound) r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -579,10 +489,10 @@ getTeamMember :: Sem r TeamMemberOptPerms getTeamMember lzusr tid uid = do m <- - E.getTeamMember tid (tUnqualified lzusr) + TeamSubsystem.internalGetTeamMember (tUnqualified lzusr) tid >>= noteS @'NotATeamMember let withPerms = (m `canSeePermsOf`) - member <- E.getTeamMember tid uid >>= noteS @'TeamMemberNotFound + member <- TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'TeamMemberNotFound pure $ setOptionalPerms withPerms member uncheckedGetTeamMember :: @@ -615,7 +525,10 @@ addTeamMember :: Member TeamFeatureStore r, Member TeamNotificationStore r, Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -630,7 +543,7 @@ addTeamMember lzusr zcon tid nmem = do . Log.field "action" (Log.val "Teams.addTeamMember") -- verify permissions zusrMembership <- - E.getTeamMember tid zusr + TeamSubsystem.internalGetTeamMember zusr tid >>= permissionCheck AddTeamMember let targetPermissions = nmem ^. nPermissions targetPermissions `ensureNotElevated` zusrMembership @@ -655,7 +568,10 @@ uncheckedAddTeamMember :: Member P.TinyLog r, Member TeamFeatureStore r, Member TeamNotificationStore r, - Member TeamStore r + Member TeamStore r, + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamJournal r ) => TeamId -> NewTeamMember -> @@ -676,7 +592,9 @@ uncheckedUpdateTeamMember :: Member NotificationSubsystem r, Member Now r, Member P.TinyLog r, - Member TeamStore r + Member TeamStore r, + Member TeamJournal r, + Member TeamSubsystem r ) => Maybe (Local UserId) -> Maybe ConnId -> @@ -695,7 +613,7 @@ uncheckedUpdateTeamMember mlzusr mZcon tid newMem = do team <- fmap tdTeam $ E.getTeam tid >>= noteS @'TeamNotFound previousMember <- - E.getTeamMember tid targetId >>= noteS @'TeamMemberNotFound + TeamSubsystem.internalGetTeamMember targetId tid >>= noteS @'TeamMemberNotFound admins <- E.getTeamAdmins tid let admins' = [targetId | isAdminOrOwner targetPermissions] <> filter (/= targetId) admins @@ -735,7 +653,9 @@ updateTeamMember :: Member NotificationSubsystem r, Member Now r, Member P.TinyLog r, - Member TeamStore r + Member TeamStore r, + Member TeamJournal r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -753,13 +673,13 @@ updateTeamMember lzusr zcon tid newMem = do -- get the team and verify permissions user <- - E.getTeamMember tid zusr + TeamSubsystem.internalGetTeamMember zusr tid >>= permissionCheck SetMemberPermissions -- user may not elevate permissions targetPermissions `ensureNotElevated` user previousMember <- - E.getTeamMember tid targetId >>= noteS @'TeamMemberNotFound + TeamSubsystem.internalGetTeamMember targetId tid >>= noteS @'TeamMemberNotFound when ( downgradesOwner previousMember targetPermissions && not (canDowngradeOwner user previousMember) @@ -791,7 +711,10 @@ deleteTeamMember :: Member ConversationSubsystem r, Member TeamFeatureStore r, Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member TeamJournal r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -817,7 +740,10 @@ deleteNonBindingTeamMember :: Member ConversationSubsystem r, Member TeamFeatureStore r, Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member TeamJournal r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -843,7 +769,10 @@ deleteTeamMember' :: Member ConversationSubsystem r, Member TeamFeatureStore r, Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member TeamJournal r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -855,8 +784,8 @@ deleteTeamMember' lusr zcon tid remove mBody = do P.debug $ Log.field "targets" (toByteString remove) . Log.field "action" (Log.val "Teams.deleteTeamMember") - zusrMember <- E.getTeamMember tid (tUnqualified lusr) - targetMember <- E.getTeamMember tid remove + zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid + targetMember <- TeamSubsystem.internalGetTeamMember remove tid void $ permissionCheck RemoveTeamMember zusrMember do dm <- noteS @'NotATeamMember zusrMember @@ -998,15 +927,15 @@ removeFromConvsAndPushConvLeaveEvent lusr zcon tid remove = do getTeamConversations :: ( Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, - Member TeamStore r, - Member ConversationStore r + Member ConversationStore r, + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r Public.TeamConversationList getTeamConversations zusr tid = do tm <- - E.getTeamMember tid zusr + TeamSubsystem.internalGetTeamMember zusr tid >>= noteS @'NotATeamMember unless (tm `hasPermission` GetTeamConversations) $ throwS @OperationDenied @@ -1016,8 +945,8 @@ getTeamConversation :: ( Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, - Member TeamStore r, - Member ConversationStore r + Member ConversationStore r, + Member TeamSubsystem r ) => UserId -> TeamId -> @@ -1025,7 +954,7 @@ getTeamConversation :: Sem r Public.TeamConversation getTeamConversation zusr tid cid = do tm <- - E.getTeamMember tid zusr + TeamSubsystem.internalGetTeamMember zusr tid >>= noteS @'NotATeamMember unless (tm `hasPermission` GetTeamConversations) $ throwS @OperationDenied @@ -1047,7 +976,8 @@ deleteTeamConversation :: Member ConversationSubsystem r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1062,13 +992,13 @@ getSearchVisibility :: ( Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, Member SearchVisibilityStore r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r TeamSearchVisibilityView getSearchVisibility luid tid = do - zusrMembership <- E.getTeamMember tid (tUnqualified luid) + zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid void $ permissionCheck ViewTeamSearchVisibility zusrMembership getSearchVisibilityInternal tid @@ -1078,7 +1008,7 @@ setSearchVisibility :: Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamSearchVisibilityNotEnabled) r, Member SearchVisibilityStore r, - Member TeamStore r + Member TeamSubsystem r ) => (TeamId -> Sem r Bool) -> Local UserId -> @@ -1086,7 +1016,7 @@ setSearchVisibility :: Public.TeamSearchVisibilityView -> Sem r () setSearchVisibility availableForTeam luid tid req = do - zusrMembership <- E.getTeamMember tid (tUnqualified luid) + zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid void $ permissionCheck ChangeTeamSearchVisibility zusrMembership setSearchVisibilityInternal availableForTeam tid req @@ -1185,9 +1115,10 @@ ensureNotTooLarge tid = do ensureNotTooLargeForLegalHold :: forall r. ( Member LegalHoldStore r, - Member TeamStore r, Member TeamFeatureStore r, - Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r + Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r, + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Int -> @@ -1246,7 +1177,9 @@ addTeamMemberInternal tid origin originConn (ntmNewTeamMember -> new) = do getBindingTeamMembers :: ( Member (ErrorS 'TeamNotFound) r, Member (ErrorS 'NonBindingTeam) r, - Member TeamStore r + Member TeamStore r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => UserId -> Sem r TeamMemberList @@ -1271,9 +1204,10 @@ canUserJoinTeam :: forall r. ( Member BrigAPIAccess r, Member LegalHoldStore r, - Member TeamStore r, Member TeamFeatureStore r, - Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r + Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r, + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Sem r () @@ -1311,7 +1245,7 @@ userIsTeamOwner :: Member (ErrorS 'AccessDenied) r, Member (ErrorS 'NotATeamMember) r, Member (Input (Local ())) r, - Member TeamStore r + Member TeamSubsystem r ) => TeamId -> UserId -> @@ -1346,9 +1280,9 @@ updateTeamCollaborator :: Member (ErrorS OperationDenied) r, Member (ErrorS NotATeamMember) r, Member P.TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member ConversationSubsystem r + Member ConversationSubsystem r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -1359,7 +1293,7 @@ updateTeamCollaborator lusr tid rusr perms = do P.debug $ Log.field "targets" (toByteString rusr) . Log.field "action" (Log.val "Teams.updateTeamCollaborator") - zusrMember <- E.getTeamMember tid (tUnqualified lusr) + zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid void $ permissionCheck UpdateTeamCollaborator zusrMember when (Set.null $ Set.intersection (Set.fromList [Collaborator.CreateTeamConversation, Collaborator.ImplicitConnection]) perms) $ removeFromConvsAndPushConvLeaveEvent lusr Nothing tid rusr @@ -1378,7 +1312,9 @@ removeTeamCollaborator :: Member P.TinyLog r, Member TeamFeatureStore r, Member TeamStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -1388,7 +1324,7 @@ removeTeamCollaborator lusr tid rusr = do P.debug $ Log.field "targets" (toByteString rusr) . Log.field "action" (Log.val "Teams.removeTeamCollaborator") - zusrMember <- E.getTeamMember tid (tUnqualified lusr) + zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid void $ permissionCheck RemoveTeamCollaborator zusrMember toNotify <- getFeatureForTeam @LimitedEventFanoutConfig tid diff --git a/services/galley/src/Galley/API/Teams/Export.hs b/services/galley/src/Galley/API/Teams/Export.hs index 229253d2eb7..51c99e5fbdb 100644 --- a/services/galley/src/Galley/API/Teams/Export.hs +++ b/services/galley/src/Galley/API/Teams/Export.hs @@ -30,9 +30,7 @@ import Data.Id import Data.Map qualified as Map import Data.Qualified (Local, tUnqualified) import Galley.Effects -import Galley.Effects.SparAccess qualified as Spar import Galley.Effects.TeamMemberStore (listTeamMembers) -import Galley.Effects.TeamStore import Imports hiding (atomicModifyIORef, newEmptyMVar, newIORef, putMVar, readMVar, takeMVar, threadDelay, tryPutMVar) import Polysemy import Polysemy.Async @@ -48,6 +46,9 @@ import Wire.Sem.Concurrency import Wire.Sem.Concurrency.IO import Wire.Sem.Paging qualified as E import Wire.Sem.Paging.Cassandra (InternalPaging) +import Wire.SparAPIAccess qualified as Spar +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem -- | Cache of inviter handles. -- @@ -84,7 +85,7 @@ lookupInviter cache uid = flip onException ensureCache $ do getUserRecord :: ( Member BrigAPIAccess r, - Member Spar.SparAccess r, + Member SparAPIAccess r, Member (ErrorS TeamMemberNotFound) r, Member (Final IO) r, Member Resource r @@ -121,15 +122,15 @@ getTeamMembersCSV :: ( Member BrigAPIAccess r, Member (ErrorS 'AccessDenied) r, Member (TeamMemberStore InternalPaging) r, - Member TeamStore r, Member (Final IO) r, - Member SparAccess r + Member SparAPIAccess r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r LowLevelStreamingBody getTeamMembersCSV lusr tid = do - getTeamMember tid (tUnqualified lusr) >>= \case + TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid >>= \case Nothing -> throwS @'AccessDenied Just member -> unless (member `hasPermission` DownloadTeamMembersCsv) $ throwS @'AccessDenied diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index d5f4ff10fd1..a8b28c6c3c0 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -44,12 +44,12 @@ import Galley.API.Error (InternalError) import Galley.API.LegalHold qualified as LegalHold import Galley.API.LegalHold.Team qualified as LegalHold import Galley.API.Teams.Features.Get -import Galley.API.Util (assertTeamExists, getTeamMembersForFanout, membersToRecipients, permissionCheck) +import Galley.API.Util (assertTeamExists, getTeamMembersForFanout, permissionCheck) import Galley.App import Galley.Effects import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData import Galley.Effects.TeamFeatureStore -import Galley.Effects.TeamStore (getLegalHoldFlag, getTeamMember) +import Galley.Env (FanoutLimit) import Galley.Options import Galley.Types.Teams import Imports @@ -73,6 +73,8 @@ import Wire.Sem.Now (Now) import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem patchFeatureInternal :: forall cfg r. @@ -84,7 +86,9 @@ patchFeatureInternal :: Member TeamStore r, Member TeamFeatureStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> LockableFeaturePatch cfg -> @@ -118,17 +122,18 @@ setFeature :: Member (ErrorS OperationDenied) r, Member (Error TeamFeatureError) r, Member (Input Opts) r, - Member TeamStore r, Member TeamFeatureStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => UserId -> TeamId -> Feature cfg -> Sem r (LockableFeature cfg) setFeature uid tid feat = do - zusrMembership <- getTeamMember tid uid + zusrMembership <- TeamSubsystem.internalGetTeamMember uid tid void $ permissionCheck ChangeTeamFeature zusrMembership setFeatureUnchecked tid feat @@ -143,7 +148,9 @@ setFeatureInternal :: Member TeamStore r, Member TeamFeatureStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> Feature cfg -> @@ -159,10 +166,11 @@ setFeatureUnchecked :: SetFeatureForTeamConstraints cfg r, Member (Error TeamFeatureError) r, Member (Input Opts) r, - Member TeamStore r, Member TeamFeatureStore r, Member (P.Logger (Log.Msg -> Log.Msg)) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> Feature cfg -> @@ -205,8 +213,9 @@ pushFeatureEvent :: forall cfg r. ( IsFeatureConfig cfg, Member NotificationSubsystem r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> Event -> @@ -243,7 +252,8 @@ setFeatureForTeam :: Member P.TinyLog r, Member NotificationSubsystem r, Member TeamFeatureStore r, - Member TeamStore r + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> LockableFeature cfg -> @@ -329,6 +339,7 @@ instance SetFeatureConfig LegalholdConfig where Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, + Member (Input (FeatureDefaults LegalholdConfig)) r, Member (Input Env) r, Member Now r, Member LegalHoldStore r, @@ -340,14 +351,16 @@ instance SetFeatureConfig LegalholdConfig where Member Random r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) prepareFeature tid feat = do -- this extra do is to encapsulate the assertions running before the actual operation. -- enabling LH for teams is only allowed in normal operation; disabled-permanently and -- whitelist-teams have no or their own way to do that, resp. - featureLegalHold <- getLegalHoldFlag + featureLegalHold <- input @(FeatureDefaults LegalholdConfig) case featureLegalHold of FeatureLegalHoldDisabledByDefault -> do pure () diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index fd19148afa6..12ef28d651b 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -47,7 +47,6 @@ import Galley.API.LegalHold.Team import Galley.API.Util import Galley.Effects import Galley.Effects.TeamFeatureStore -import Galley.Effects.TeamStore (getOneUserTeam, getTeamMember) import Galley.Options import Galley.Types.Teams import Imports @@ -61,6 +60,9 @@ import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Mul import Wire.API.Team.Feature import Wire.BrigAPIAccess (getAccountConferenceCallingConfigClient) import Wire.ConversationStore as ConversationStore +import Wire.TeamStore qualified as TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem data DoAuth = DoAuth UserId | DontDoAuth @@ -118,13 +120,13 @@ getFeature :: Member (Input Opts) r, Member TeamFeatureStore r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r (LockableFeature cfg) getFeature uid tid = do - void $ getTeamMember tid uid >>= noteS @'NotATeamMember + void $ TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'NotATeamMember getFeatureForTeam @cfg tid getFeatureInternal :: @@ -147,14 +149,15 @@ toTeamStatus tid feat = Multi.TeamStatus tid feat.status getTeamAndCheckMembership :: ( Member TeamStore r, Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r + Member (ErrorS 'TeamNotFound) r, + Member TeamSubsystem r ) => UserId -> Sem r (Maybe TeamId) getTeamAndCheckMembership uid = do - mTid <- getOneUserTeam uid + mTid <- TeamStore.getOneUserTeam uid for_ mTid $ \tid -> do - zusrMembership <- getTeamMember tid uid + zusrMembership <- TeamSubsystem.internalGetTeamMember uid tid void $ maybe (throwS @'NotATeamMember) pure zusrMembership assertTeamExists tid pure mTid @@ -165,13 +168,15 @@ getAllTeamFeaturesForTeam :: Member (ErrorS 'NotATeamMember) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r AllTeamFeatures getAllTeamFeaturesForTeam luid tid = do - void $ getTeamMember tid (tUnqualified luid) >>= noteS @'NotATeamMember + void $ TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid >>= noteS @'NotATeamMember getAllTeamFeatures tid class @@ -196,7 +201,8 @@ getAllTeamFeatures :: ( Member (Input Opts) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Sem r AllTeamFeatures @@ -225,7 +231,9 @@ getAllTeamFeaturesForUser :: Member (Input Opts) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => UserId -> Sem r AllTeamFeatures @@ -244,7 +252,8 @@ getSingleFeatureForUser :: Member TeamStore r, Member TeamFeatureStore r, GetFeatureForUserConstraints cfg r, - ComputeFeatureConstraints cfg r + ComputeFeatureConstraints cfg r, + Member TeamSubsystem r ) => UserId -> Sem r (LockableFeature cfg) @@ -310,13 +319,17 @@ instance GetFeatureConfig LegalholdConfig where Member TeamFeatureStore r, Member LegalHoldStore r, Member TeamStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, Member (ErrorS OperationDenied) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r ) type ComputeFeatureConstraints LegalholdConfig r = - (Member TeamStore r, Member LegalHoldStore r) + ( Member TeamStore r, + Member LegalHoldStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r + ) computeFeature tid defFeature dbFeature = do status <- computeLegalHoldFeatureStatus tid dbFeature diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 8888ac375c1..c621043dba2 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -1,19 +1,3 @@ --- 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 . {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE RecordWildCards #-} @@ -119,7 +103,7 @@ import Galley.Effects import Galley.Effects.ClientStore qualified as E import Galley.Effects.CodeStore qualified as E import Galley.Effects.FederatorAccess qualified as E -import Galley.Effects.TeamStore qualified as E +import Galley.Env import Galley.Options import Imports hiding (forkIO) import Polysemy @@ -160,6 +144,8 @@ import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserGroupStore (UserGroupStore, getUserGroupsForConv) import Wire.UserList @@ -303,7 +289,8 @@ updateConversationAccess :: ( Members UpdateConversationAccessEffects r, Member Now r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -319,7 +306,8 @@ updateConversationAccessUnqualified :: ( Members UpdateConversationAccessEffects r, Member Now r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -350,9 +338,9 @@ updateConversationReceiptMode :: Member ConversationSubsystem r, Member (Input (Local ())) r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -431,9 +419,9 @@ updateConversationReceiptModeUnqualified :: Member ConversationSubsystem r, Member (Input (Local ())) r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -450,9 +438,9 @@ updateConversationMessageTimer :: Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -483,9 +471,9 @@ updateConversationMessageTimerUnqualified :: Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -509,7 +497,8 @@ deleteLocalConversation :: Member ProposalStore r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -535,7 +524,7 @@ addCodeUnqualifiedWithReqBody :: Member (Input Opts) r, Member TeamFeatureStore r, Member RateLimit r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> Maybe Text -> @@ -561,7 +550,7 @@ addCodeUnqualified :: Member HashPassword r, Member TeamFeatureStore r, Member RateLimit r, - Member TeamStore r + Member TeamSubsystem r ) => Maybe CreateConversationCodeRequest -> UserId -> @@ -589,7 +578,7 @@ addCode :: Member (Input Opts) r, Member TeamFeatureStore r, Member RateLimit r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> Maybe ZHostValue -> @@ -600,7 +589,7 @@ addCode :: addCode lusr mbZHost mZcon lcnv mReq = do conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled (convTeam conv) - mTeamMember <- maybe (pure Nothing) (flip E.getTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam + mTeamMember <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam Query.ensureConvAdmin conv (tUnqualified lusr) mTeamMember ensureAccess conv CodeAccess ensureGuestsOrNonTeamMembersAllowed conv @@ -639,7 +628,7 @@ rmCodeUnqualified :: Member NotificationSubsystem r, Member (Input (Local ())) r, Member Now r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -657,7 +646,7 @@ rmCode :: Member ExternalAccess r, Member NotificationSubsystem r, Member Now r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -666,7 +655,7 @@ rmCode :: rmCode lusr zcon lcnv = do conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound - mTeamMember <- maybe (pure Nothing) (flip E.getTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam + mTeamMember <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam Query.ensureConvAdmin conv (tUnqualified lusr) mTeamMember ensureAccess conv CodeAccess let (bots, users) = localBotsAndUsers $ conv.localMembers @@ -751,9 +740,9 @@ updateConversationProtocolWithLocalUser :: Member Random r, Member ProposalStore r, Member TeamFeatureStore r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -787,7 +776,6 @@ updateChannelAddPermission :: Member ExternalAccess r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member TeamStore r, Member (Input (Local ())) r, Member TinyLog r, Member (ErrorS (MissingPermission Nothing)) r, @@ -799,7 +787,8 @@ updateChannelAddPermission :: Member FederatorAccess r, Member (ErrorS 'InvalidTargetAccess) r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -837,10 +826,10 @@ joinConversationByReusableCode :: Member (ErrorS 'TooManyMembers) r, Member ConversationSubsystem r, Member (Input Opts) r, - Member TeamStore r, Member TeamFeatureStore r, Member HashPassword r, - Member RateLimit r + Member RateLimit r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -863,7 +852,7 @@ joinConversationById :: Member (ErrorS 'TooManyMembers) r, Member ConversationSubsystem r, Member (Input Opts) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -883,7 +872,7 @@ joinConversation :: Member ConversationSubsystem r, Member (Input Opts) r, Member ConversationStore r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -952,7 +941,8 @@ addMembers :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -967,7 +957,7 @@ addMembers lusr zcon qcnv (InviteQualified users role) = do mapErrorS @OperationDenied @('ActionDenied 'AddConversationMember) $ forM_ conv.metadata.cnvmTeam $ \tid -> do forM_ users $ \u -> do - mTeamMembership <- E.getTeamMember tid $ qUnqualified u + mTeamMembership <- TeamSubsystem.internalGetTeamMember (qUnqualified u) tid forM_ (mTeamMembership >>= permissionsRole . Wire.API.Team.Member.getPermissions) $ permissionCheck JoinRegularConversations . Just @@ -1007,7 +997,8 @@ addMembersUnqualifiedV2 :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1051,7 +1042,8 @@ addMembersUnqualified :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1099,7 +1091,8 @@ replaceMembers :: Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, Member UserGroupStore r, - Member ConversationSubsystem r + Member ConversationSubsystem r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1115,7 +1108,7 @@ replaceMembers lusr zcon qcnv (InviteQualified invitedUsers role) = do mapErrorS @OperationDenied @('ActionDenied 'AddConversationMember) $ forM_ conv.metadata.cnvmTeam $ \tid -> do forM_ invitedUsers $ \u -> do - mTeamMembership <- E.getTeamMember tid $ qUnqualified u + mTeamMembership <- TeamSubsystem.internalGetTeamMember (qUnqualified u) tid forM_ (mTeamMembership >>= permissionsRole . Wire.API.Team.Member.getPermissions) $ permissionCheck JoinRegularConversations . Just @@ -1226,9 +1219,9 @@ updateOtherMemberLocalConv :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local ConvId -> Local UserId -> @@ -1252,9 +1245,9 @@ updateOtherMemberUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1277,9 +1270,9 @@ updateOtherMember :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1318,9 +1311,9 @@ removeMemberUnqualified :: Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1349,9 +1342,9 @@ removeMemberQualified :: Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1427,9 +1420,9 @@ removeMemberFromLocalConv :: Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local ConvId -> Local UserId -> @@ -1463,7 +1456,6 @@ removeMemberFromLocalConv lcnv lusr con victim removeMemberFromChannel :: forall r. ( Member (ErrorS 'ConvNotFound) r, - Member TeamStore r, Member (Input Env) r, Member (Error NoChanges) r, Member ProposalStore r, @@ -1477,7 +1469,8 @@ removeMemberFromChannel :: Member TinyLog r, Member (Error FederationError) r, Member BackendNotificationQueueAccess r, - Member ConversationStore r + Member ConversationStore r, + Member TeamSubsystem r ) => Qualified UserId -> Local StoredConversation -> @@ -1493,7 +1486,7 @@ removeMemberFromChannel qusr lconv victim = do kickMember qusr lconv notificationTargets victim where getTeamMembership :: StoredConversation -> Local UserId -> Sem r (Maybe TeamMember) - getTeamMembership conv luid = maybe (pure Nothing) (`E.getTeamMember` tUnqualified luid) conv.metadata.cnvmTeam + getTeamMembership conv luid = maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified luid)) conv.metadata.cnvmTeam -- OTR @@ -1507,8 +1500,8 @@ postProteusMessage :: Member ExternalAccess r, Member (Input Opts) r, Member Now r, - Member TeamStore r, - Member TinyLog r + Member TinyLog r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1533,7 +1526,9 @@ postProteusBroadcast :: Member (Input Opts) r, Member Now r, Member TeamStore r, - Member TinyLog r + Member TinyLog r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1582,9 +1577,9 @@ postBotMessageUnqualified :: Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input Opts) r, - Member TeamStore r, Member TinyLog r, - Member Now r + Member Now r, + Member TeamSubsystem r ) => BotId -> ConvId -> @@ -1613,7 +1608,9 @@ postOtrBroadcastUnqualified :: Member (Input Opts) r, Member Now r, Member TeamStore r, - Member TinyLog r + Member TinyLog r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1636,8 +1633,8 @@ postOtrMessageUnqualified :: Member NotificationSubsystem r, Member (Input Opts) r, Member Now r, - Member TeamStore r, - Member TinyLog r + Member TinyLog r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1663,7 +1660,8 @@ updateConversationName :: Member ConversationSubsystem r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1689,7 +1687,8 @@ updateUnqualifiedConversationName :: Member ConversationSubsystem r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1711,7 +1710,8 @@ updateLocalConversationName :: Member ConversationSubsystem r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1729,7 +1729,7 @@ memberTyping :: Member Now r, Member ConversationStore r, Member FederatorAccess r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1767,7 +1767,7 @@ memberTypingUnqualified :: Member Now r, Member ConversationStore r, Member FederatorAccess r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 52d37be558f..3107aa96d19 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -47,9 +47,7 @@ import Galley.Effects import Galley.Effects.ClientStore import Galley.Effects.CodeStore import Galley.Effects.FederatorAccess -import Galley.Effects.LegalHoldStore -import Galley.Effects.TeamStore -import Galley.Effects.TeamStore qualified as E +import Galley.Env import Galley.Options import Galley.Types.Clients (Clients, fromUserClients) import Galley.Types.Conversations.Roles @@ -94,12 +92,17 @@ import Wire.ConversationStore import Wire.ExternalAccess import Wire.HashPassword (HashPassword) import Wire.HashPassword qualified as HashPassword +import Wire.LegalHoldStore import Wire.NotificationSubsystem import Wire.RateLimit import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem +import Wire.TeamSubsystem qualified as TeamSubsytem import Wire.UserList data NoChanges = NoChanges @@ -130,7 +133,8 @@ ensureConnectedOrSameTeam :: ( Member BrigAPIAccess r, Member (ErrorS 'NotConnected) r, Member TeamStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> [Qualified UserId] -> @@ -151,7 +155,8 @@ ensureConnectedToLocalsOrSameTeam :: ( Member BrigAPIAccess r, Member (ErrorS 'NotConnected) r, Member TeamStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> [UserId] -> @@ -163,7 +168,7 @@ ensureConnectedToLocalsOrSameTeam (tUnqualified -> u) uids = do icUsers <- getTeamCollaborators uTeams -- We collect all the relevant uids from same teams as the origin user sameTeamUids <- forM (uTeams `union` icTeams) $ \team -> - fmap (view Mem.userId) <$> selectTeamMembers team uids + fmap (view Mem.userId) <$> TeamSubsytem.internalSelectTeamMembers team uids -- Do not check connections for users that are on the same team ensureConnectedToLocals u ((uids \\ join sameTeamUids) \\ icUsers) where @@ -381,13 +386,13 @@ assertTeamExists tid = do assertOnTeam :: ( Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r () assertOnTeam uid tid = - getTeamMember tid uid >>= \case + TeamSubsystem.internalGetTeamMember uid tid >>= \case Nothing -> throwS @'NotATeamMember Just _ -> pure () @@ -441,12 +446,6 @@ acceptOne2One lusr conv conn = do acceptConnectConversation cid pure $ Data.convSetType One2OneConv conv -localMemberToRecipient :: LocalMember -> Recipient -localMemberToRecipient = userRecipient . (.id_) - -userRecipient :: UserId -> Recipient -userRecipient u = Recipient u PushV2.RecipientClientsAll - memberJoinEvent :: Local UserId -> Qualified ConvId -> @@ -601,25 +600,6 @@ bmFromMembers lmems rusers = case localBotsAndUsers lmems of convBotsAndMembers :: StoredConversation -> BotsAndMembers convBotsAndMembers conv = bmFromMembers (conv.localMembers) (conv.remoteMembers) -localBotsAndUsers :: (Foldable f) => f LocalMember -> ([BotMember], [LocalMember]) -localBotsAndUsers = foldMap botOrUser - where - botOrUser m = case m.service of - -- we drop invalid bots here, which shouldn't happen - Just _ -> (toList (newBotMember m), []) - Nothing -> ([], [m]) - -nonTeamMembers :: [LocalMember] -> [TeamMember] -> [LocalMember] -nonTeamMembers cm tm = filter (not . isMemberOfTeam . (.id_)) cm - where - -- FUTUREWORK: remote members: teams and their members are always on the same backend - isMemberOfTeam = \case - uid -> isTeamMember uid tm - -membersToRecipients :: Maybe UserId -> [TeamMember] -> [Recipient] -membersToRecipients Nothing = map (userRecipient . view Mem.userId) -membersToRecipients (Just u) = map userRecipient . filter (/= u) . map (view Mem.userId) - getSelfMemberFromLocals :: (Foldable t, Member (ErrorS 'ConvNotFound) r) => UserId -> @@ -660,7 +640,7 @@ getConversationAsMember :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Qualified UserId -> Local ConvId -> @@ -676,7 +656,7 @@ getConversationAsViewer :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Qualified UserId -> Local ConvId -> @@ -692,7 +672,7 @@ getConversationAsViewer qusr lcnv = do =<< runMaybeT ( do uid <- hoistMaybe $ foldQualified lcnv (Just . tUnqualified) (const Nothing) qusr - tm <- MaybeT $ E.getTeamMember tid uid + tm <- MaybeT $ TeamSubsystem.internalGetTeamMember uid tid guard $ hasManageChannelsPermission c tm ) (Nothing, Nothing) -> throwAccessDenied @@ -786,7 +766,7 @@ ensureConversationAccess :: ( Member BrigAPIAccess r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> StoredConversation -> @@ -794,7 +774,7 @@ ensureConversationAccess :: Sem r () ensureConversationAccess zusr conv access = do ensureAccess conv access - zusrMembership <- maybe (pure Nothing) (`getTeamMember` zusr) (Data.convTeam conv) + zusrMembership <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember zusr) (Data.convTeam conv) ensureAccessRole (Data.convAccessRoles conv) [(zusr, zusrMembership)] ensureAccess :: @@ -1049,7 +1029,7 @@ consentGiven = \case UserLegalHoldNoConsent -> ConsentNotGiven checkConsent :: - (Member TeamStore r) => + (Member TeamSubsystem r) => Map UserId TeamId -> UserId -> Sem r ConsentGiven @@ -1059,7 +1039,7 @@ checkConsent teamsOfUsers other = do -- Get legalhold status of user. Defaults to 'defUserLegalHoldStatus' if user -- doesn't belong to a team. getLHStatus :: - (Member TeamStore r) => + (Member TeamSubsystem r) => Maybe TeamId -> UserId -> Sem r UserLegalHoldStatus @@ -1067,12 +1047,13 @@ getLHStatus teamOfUser other = do case teamOfUser of Nothing -> pure defUserLegalHoldStatus Just team -> do - mMember <- getTeamMember team other + mMember <- TeamSubsystem.internalGetTeamMember other team pure $ maybe defUserLegalHoldStatus (view legalHoldStatus) mMember anyLegalholdActivated :: ( Member (Input Opts) r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => [UserId] -> Sem r Bool @@ -1091,7 +1072,8 @@ anyLegalholdActivated uids = do allLegalholdConsentGiven :: ( Member (Input Opts) r, Member LegalHoldStore r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => [UserId] -> Sem r Bool @@ -1116,7 +1098,7 @@ allLegalholdConsentGiven uids = do -- | Add to every uid the legalhold status getLHStatusForUsers :: - (Member TeamStore r) => + (Member TeamStore r, Member TeamSubsystem r) => [UserId] -> Sem r [(UserId, UserLegalHoldStatus)] getLHStatusForUsers uids = @@ -1129,10 +1111,15 @@ getLHStatusForUsers uids = (uid,) <$> getLHStatus (Map.lookup uid teamsOfUsers) uid ) -getTeamMembersForFanout :: (Member TeamStore r) => TeamId -> Sem r TeamMemberList +getTeamMembersForFanout :: + ( Member (Input FanoutLimit) r, + Member TeamSubsystem r + ) => + TeamId -> + Sem r TeamMemberList getTeamMembersForFanout tid = do - lim <- fanoutLimit - getTeamMembersWithLimit tid lim + lim <- input + TeamSubsystem.internalGetTeamMembersWithLimit tid (Just lim) ensureMemberLimit :: ( Foldable f, diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 471b92b44b2..aef93717a16 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -53,26 +53,29 @@ import Data.Qualified import Data.Range import Data.Text qualified as Text import Galley.API.Error -import Galley.Aws qualified as Aws import Galley.Cassandra.Client import Galley.Cassandra.Code import Galley.Cassandra.CustomBackend -import Galley.Cassandra.LegalHold import Galley.Cassandra.Proposal import Galley.Cassandra.SearchVisibility import Galley.Cassandra.Team + ( interpretInternalTeamListToCassandra, + interpretTeamListToCassandra, + interpretTeamMemberStoreToCassandra, + interpretTeamMemberStoreToCassandraWithPaging, + ) import Galley.Cassandra.TeamFeatures import Galley.Cassandra.TeamNotifications import Galley.Effects import Galley.Env -import Galley.Intra.Effects +import Galley.External.LegalHoldService.Internal qualified as LHInternal import Galley.Intra.Federator import Galley.Keys +import Galley.Monad (runApp) import Galley.Options hiding (brig, endpoint, federator) import Galley.Options qualified as O import Galley.Queue import Galley.Queue qualified as Q -import Galley.TeamSubsystem (interpretTeamSubsystem) import Galley.Types.Teams import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) import Hasql.Pool qualified as Hasql @@ -104,6 +107,7 @@ import Wire.API.Error import Wire.API.Federation.Error import Wire.API.Team.Collaborator import Wire.API.Team.Feature +import Wire.AWS qualified as Aws import Wire.BackendNotificationQueueAccess.RabbitMq qualified as BackendNotificationQueueAccess import Wire.BrigAPIAccess.Rpc import Wire.ConversationStore.Cassandra @@ -114,6 +118,8 @@ import Wire.ExternalAccess.External import Wire.FireAndForget import Wire.GundeckAPIAccess (runGundeckAPIAccess) import Wire.HashPassword.Interpreter +import Wire.LegalHoldStore.Cassandra (interpretLegalHoldStoreToCassandra) +import Wire.LegalHoldStore.Env (LegalHoldEnv (..)) import Wire.NotificationSubsystem.Interpreter (runNotificationSubsystemGundeck) import Wire.ParseException import Wire.RateLimit @@ -123,8 +129,12 @@ import Wire.Sem.Delay import Wire.Sem.Now.IO (nowToIO) import Wire.Sem.Random.IO import Wire.ServiceStore.Cassandra (interpretServiceStoreToCassandra) +import Wire.SparAPIAccess.Rpc import Wire.TeamCollaboratorsStore.Postgres (interpretTeamCollaboratorsStoreToPostgres) import Wire.TeamCollaboratorsSubsystem.Interpreter +import Wire.TeamJournal.Aws +import Wire.TeamStore.Cassandra (interpretTeamStoreToCassandra) +import Wire.TeamSubsystem.Interpreter import Wire.UserGroupStore.Postgres (interpretUserGroupStoreToPostgres) -- Effects needed by the interpretation of other effects @@ -193,7 +203,7 @@ createEnv o l = do Env (RequestId defRequestId) o l mgr h2mgr (o ^. O.federator) (o ^. O.brig) cass postgres <$> Q.new 16000 <*> initExtEnv disableTlsV1 - <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. journal) + <*> maybe (pure Nothing) (\jo -> fmap Just (Aws.mkEnv l mgr (jo ^. O.endpoint) (jo ^. O.queueName))) (o ^. journal) <*> traverse loadAllMLSKeys (o ^. settings . mlsPrivateKeyPaths) <*> traverse (mkRabbitMqChannelMVar l (Just "galley")) (o ^. rabbitmq) <*> pure codeURIcfg @@ -274,6 +284,10 @@ evalGalley e = MigrationToPostgresql -> interpretConversationStoreToCassandraAndPostgres (e ^. cstate) PostgresqlStorage -> interpretConversationStoreToPostgres localUnit = toLocalUnsafe (e ^. options . settings . federationDomain) () + teamSubsystemConfig = + TeamSubsystemConfig + { concurrentDeletionEvents = fromMaybe defConcurrentDeletionEvents e._options._settings._concurrentDeletionEvents + } backendNotificationQueueAccessEnv = case e._rabbitmqChannel of Nothing -> Nothing @@ -316,6 +330,7 @@ evalGalley e = . runInputConst localUnit . interpretTeamFeatureSpecialContext e . runInputSem getAllTeamFeaturesForServer + . runInputConst (currentFanoutLimit (e ^. options)) . interpretInternalTeamListToCassandra . interpretTeamListToCassandra . interpretTeamMemberStoreToCassandraWithPaging lh @@ -323,12 +338,14 @@ evalGalley e = . interpretTeamFeatureStoreToCassandra . interpretMLSCommitLockStoreToCassandra (e ^. cstate) . convStoreInterpreter - . interpretTeamStoreToCassandra lh . interpretTeamNotificationStoreToCassandra . interpretServiceStoreToCassandra (e ^. cstate) . interpretUserGroupStoreToPostgres - . interpretSearchVisibilityStoreToCassandra + . runInputConst legalHoldEnv . interpretLegalHoldStoreToCassandra lh + . interpretTeamJournal (e ^. aEnv) + . interpretTeamStoreToCassandra + . interpretSearchVisibilityStoreToCassandra . interpretCustomBackendStoreToCassandra . randomToIO . runHashPassword e._options._settings._passwordHashingOptions @@ -342,19 +359,22 @@ evalGalley e = . interpretFederatorAccess . runRpcWithHttp (e ^. manager) (e ^. reqId) . runGundeckAPIAccess (e ^. options . gundeck) - . interpretTeamSubsystem . interpretBrigAccess (e ^. brig) . interpretExternalAccess (e ^. extEnv) . runNotificationSubsystemGundeck (notificationSubsystemConfig e) + . interpretSparAPIAccessToRpc (e ^. options . spar) + . interpretTeamSubsystem teamSubsystemConfig . interpretConversationSubsystem . interpretTeamCollaboratorsSubsystem - . interpretSparAccess where lh = view (options . settings . featureFlags . to npProject) e + legalHoldEnv = + let makeReq fpr url rb = runApp e (LHInternal.makeVerifiedRequest fpr url rb) + makeReqFresh fpr url rb = runApp e (LHInternal.makeVerifiedRequestFreshManager fpr url rb) + in LegalHoldEnv {makeVerifiedRequest = makeReq, makeVerifiedRequestFreshManager = makeReqFresh} -interpretTeamFeatureSpecialContext :: Env -> Sem (Input (Maybe [TeamId], FeatureDefaults LegalholdConfig) ': r) a -> Sem r a +interpretTeamFeatureSpecialContext :: Env -> Sem (Input (FeatureDefaults LegalholdConfig) ': r) a -> Sem r a interpretTeamFeatureSpecialContext e = runInputConst - ( e ^. options . settings . exposeInvitationURLsTeamAllowlist, - e ^. options . settings . featureFlags . to npProject + ( e ^. options . settings . featureFlags . to npProject ) diff --git a/services/galley/src/Galley/Aws.hs b/services/galley/src/Galley/Aws.hs deleted file mode 100644 index 67963a7e908..00000000000 --- a/services/galley/src/Galley/Aws.hs +++ /dev/null @@ -1,194 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE TemplateHaskell #-} - --- 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 Galley.Aws - ( Env, - mkEnv, - awsEnv, - eventQueue, - QueueUrl (..), - Amazon, - execute, - enqueue, - - -- * Errors - Error (..), - ) -where - -import Amazonka qualified as AWS -import Amazonka.SQS qualified as SQS -import Amazonka.SQS.Lens qualified as SQS -import Control.Lens hiding ((.=)) -import Control.Monad.Catch -import Control.Monad.Trans.Resource -import Control.Retry (exponentialBackoff, limitRetries, retrying) -import Data.ByteString.Base64 qualified as B64 -import Data.ByteString.Builder (toLazyByteString) -import Data.ProtoLens.Encoding -import Data.Text.Encoding (decodeLatin1) -import Data.UUID (toText) -import Data.UUID.V4 -import Galley.Options -import Imports -import Network.HTTP.Client - ( HttpException (..), - HttpExceptionContent (..), - Manager, - ) -import Network.TLS qualified as TLS -import Proto.TeamEvents qualified as E -import System.Logger qualified as Logger -import System.Logger.Class -import Util.Options hiding (endpoint) - -newtype QueueUrl = QueueUrl Text - deriving (Show) - -data Error where - GeneralError :: (Show e, AWS.AsError e) => e -> Error - -deriving instance Show Error - -deriving instance Typeable Error - -instance Exception Error - -data Env = Env - { _awsEnv :: !AWS.Env, - _logger :: !Logger, - _eventQueue :: !QueueUrl - } - -makeLenses ''Env - -newtype Amazon a = Amazon - { unAmazon :: ReaderT Env (ResourceT IO) a - } - deriving - ( Functor, - Applicative, - Monad, - MonadIO, - MonadThrow, - MonadCatch, - MonadMask, - MonadReader Env, - MonadResource, - MonadUnliftIO - ) - -instance MonadLogger Amazon where - log l m = view logger >>= \g -> Logger.log g l m - -mkEnv :: Logger -> Manager -> JournalOpts -> IO Env -mkEnv lgr mgr opts = do - let g = Logger.clone (Just "aws.galley") lgr - e <- mkAwsEnv g - q <- getQueueUrl e (opts ^. queueName) - pure (Env e g q) - where - sqs e = AWS.setEndpoint (e ^. awsSecure) (e ^. awsHost) (e ^. awsPort) SQS.defaultService - mkAwsEnv g = do - baseEnv <- - AWS.newEnv AWS.discover - <&> AWS.configureService (sqs (opts ^. endpoint)) - pure $ - baseEnv - { AWS.logger = awsLogger g, - AWS.retryCheck = retryCheck, - AWS.manager = mgr - } - awsLogger g l = Logger.log g (mapLevel l) . Logger.msg . toLazyByteString - mapLevel AWS.Info = Logger.Info - -- Debug output from amazonka can be very useful for tracing requests - -- but is very verbose (and multiline which we don't handle well) - -- distracting from our own debug logs, so we map amazonka's 'Debug' - -- level to our 'Trace' level. - mapLevel AWS.Debug = Logger.Trace - mapLevel AWS.Trace = Logger.Trace - -- n.b. Errors are either returned or thrown. In both cases they will - -- already be logged if left unhandled. We don't want errors to be - -- logged inside amazonka already, before we even had a chance to handle - -- them, which results in distracting noise. For debugging purposes, - -- they are still revealed on debug level. - mapLevel AWS.Error = Logger.Debug - -- TODO: Remove custom retryCheck? Should be fixed since tls 1.3.9? - -- account occasional TLS handshake failures. - -- See: https://github.com/vincenthz/hs-tls/issues/124 - -- See: https://github.com/brendanhay/amazonka/issues/269 - retryCheck _ InvalidUrlException {} = False - retryCheck n (HttpExceptionRequest _ ex) = case ex of - _ | n >= 3 -> False - NoResponseDataReceived -> True - ConnectionTimeout -> True - ConnectionClosed -> True - ConnectionFailure _ -> True - InternalException x -> case fromException x of - Just TLS.HandshakeFailed {} -> True - _ -> False - _ -> False - getQueueUrl :: AWS.Env -> Text -> IO QueueUrl - getQueueUrl e q = do - x <- - runResourceT $ - AWS.trying AWS._Error $ - AWS.send e (SQS.newGetQueueUrl q) - either - (throwM . GeneralError) - (pure . QueueUrl . view SQS.getQueueUrlResponse_queueUrl) - x - -execute :: (MonadIO m) => Env -> Amazon a -> m a -execute e m = liftIO $ runResourceT (runReaderT (unAmazon m) e) - -enqueue :: E.TeamEvent -> Amazon () -enqueue e = do - QueueUrl url <- view eventQueue - rnd <- liftIO nextRandom - amaznkaEnv <- view awsEnv - res <- retrying (limitRetries 5 <> exponentialBackoff 1000000) (const canRetry) $ const (sendCatch amaznkaEnv (req url rnd)) - either (throwM . GeneralError) (const (pure ())) res - where - event = decodeLatin1 $ B64.encode $ encodeMessage e - req url dedup = - SQS.newSendMessage url event - & SQS.sendMessage_messageGroupId ?~ "team.events" - & SQS.sendMessage_messageDeduplicationId ?~ toText dedup - --------------------------------------------------------------------------------- --- Utilities - -sendCatch :: - ( AWS.AWSRequest r, - Typeable r, - Typeable (AWS.AWSResponse r) - ) => - AWS.Env -> - r -> - Amazon (Either AWS.Error (AWS.AWSResponse r)) -sendCatch e = AWS.trying AWS._Error . AWS.send e - -canRetry :: (MonadIO m) => Either AWS.Error a -> m Bool -canRetry (Right _) = pure False -canRetry (Left e) = case e of - AWS.TransportError (HttpExceptionRequest _ ResponseTimeout) -> pure True - AWS.ServiceError se | se ^. AWS.serviceError_code == AWS.ErrorCode "RequestThrottled" -> pure True - _ -> pure False diff --git a/services/galley/src/Galley/Cassandra/LegalHold.hs b/services/galley/src/Galley/Cassandra/LegalHold.hs deleted file mode 100644 index 5a7c30acfa0..00000000000 --- a/services/galley/src/Galley/Cassandra/LegalHold.hs +++ /dev/null @@ -1,197 +0,0 @@ --- 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 Galley.Cassandra.LegalHold - ( interpretLegalHoldStoreToCassandra, - isTeamLegalholdWhitelisted, - - -- * Used by tests - selectPendingPrekeys, - validateServiceKey, - ) -where - -import Brig.Types.Instances () -import Brig.Types.Team.LegalHold -import Cassandra -import Control.Exception.Enclosed (handleAny) -import Control.Lens (unsnoc) -import Data.ByteString.Conversion.To -import Data.ByteString.Lazy.Char8 qualified as LC8 -import Data.Id -import Data.LegalHold -import Data.Misc -import Galley.Cassandra.Queries qualified as Q -import Galley.Cassandra.Store -import Galley.Cassandra.Util -import Galley.Effects.LegalHoldStore (LegalHoldStore (..)) -import Galley.Env -import Galley.External.LegalHoldService.Internal -import Galley.Monad -import Galley.Types.Teams -import Imports hiding (unsnoc) -import OpenSSL.EVP.Digest qualified as SSL -import OpenSSL.EVP.PKey qualified as SSL -import OpenSSL.PEM qualified as SSL -import OpenSSL.RSA qualified as SSL -import Polysemy -import Polysemy.Input -import Polysemy.TinyLog -import Ssl.Util qualified as SSL -import Wire.API.Provider.Service -import Wire.API.Team.Feature -import Wire.API.User.Client.Prekey -import Wire.ConversationStore.Cassandra.Instances () - -interpretLegalHoldStoreToCassandra :: - ( Member (Embed IO) r, - Member (Input ClientState) r, - Member (Input Env) r, - Member TinyLog r - ) => - FeatureDefaults LegalholdConfig -> - Sem (LegalHoldStore ': r) a -> - Sem r a -interpretLegalHoldStoreToCassandra lh = interpret $ \case - CreateSettings s -> do - logEffect "LegalHoldStore.CreateSettings" - embedClient $ createSettings s - GetSettings tid -> do - logEffect "LegalHoldStore.GetSettings" - embedClient $ getSettings tid - RemoveSettings tid -> do - logEffect "LegalHoldStore.RemoveSettings" - embedClient $ removeSettings tid - InsertPendingPrekeys uid pkeys -> do - logEffect "LegalHoldStore.InsertPendingPrekeys" - embedClient $ insertPendingPrekeys uid pkeys - SelectPendingPrekeys uid -> do - logEffect "LegalHoldStore.SelectPendingPrekeys" - embedClient $ selectPendingPrekeys uid - DropPendingPrekeys uid -> do - logEffect "LegalHoldStore.DropPendingPrekeys" - embedClient $ dropPendingPrekeys uid - SetUserLegalHoldStatus tid uid st -> do - logEffect "LegalHoldStore.SetUserLegalHoldStatus" - embedClient $ setUserLegalHoldStatus tid uid st - SetTeamLegalholdWhitelisted tid -> do - logEffect "LegalHoldStore.SetTeamLegalholdWhitelisted" - embedClient $ setTeamLegalholdWhitelisted tid - UnsetTeamLegalholdWhitelisted tid -> do - logEffect "LegalHoldStore.UnsetTeamLegalholdWhitelisted" - embedClient $ unsetTeamLegalholdWhitelisted tid - IsTeamLegalholdWhitelisted tid -> do - logEffect "LegalHoldStore.IsTeamLegalholdWhitelisted" - embedClient $ isTeamLegalholdWhitelisted lh tid - -- FUTUREWORK: should this action be part of a separate effect? - MakeVerifiedRequestFreshManager fpr url r -> do - logEffect "LegalHoldStore.MakeVerifiedRequestFreshManager" - embedApp $ makeVerifiedRequestFreshManager fpr url r - MakeVerifiedRequest fpr url r -> do - logEffect "LegalHoldStore.MakeVerifiedRequest" - embedApp $ makeVerifiedRequest fpr url r - ValidateServiceKey sk -> do - logEffect "LegalHoldStore.ValidateServiceKey" - embed @IO $ validateServiceKey sk - --- | Returns 'False' if legal hold is not enabled for this team --- The Caller is responsible for checking whether legal hold is enabled for this team -createSettings :: (MonadClient m) => LegalHoldService -> m () -createSettings (LegalHoldService tid url fpr tok key) = do - retry x1 $ write Q.insertLegalHoldSettings (params LocalQuorum (url, fpr, tok, key, tid)) - --- | Returns 'Nothing' if no settings are saved --- The Caller is responsible for checking whether legal hold is enabled for this team -getSettings :: (MonadClient m) => TeamId -> m (Maybe LegalHoldService) -getSettings tid = - fmap toLegalHoldService <$> do - retry x1 $ query1 Q.selectLegalHoldSettings (params LocalQuorum (Identity tid)) - where - toLegalHoldService (httpsUrl, fingerprint, tok, key) = LegalHoldService tid httpsUrl fingerprint tok key - -removeSettings :: (MonadClient m) => TeamId -> m () -removeSettings tid = retry x5 (write Q.removeLegalHoldSettings (params LocalQuorum (Identity tid))) - -insertPendingPrekeys :: (MonadClient m) => UserId -> [Prekey] -> m () -insertPendingPrekeys uid keys = retry x5 . batch $ - forM_ keys $ - \key -> - addPrepQuery Q.insertPendingPrekeys (toTuple key) - where - toTuple (Prekey keyId key) = (uid, keyId, key) - -selectPendingPrekeys :: (MonadClient m) => UserId -> m (Maybe ([Prekey], LastPrekey)) -selectPendingPrekeys uid = - pickLastKey . fmap fromTuple - <$> retry x1 (query Q.selectPendingPrekeys (params LocalQuorum (Identity uid))) - where - fromTuple (keyId, key) = Prekey keyId key - pickLastKey allPrekeys = - case unsnoc allPrekeys of - Nothing -> Nothing - Just (keys, lst) -> pure (keys, lastPrekey . prekeyKey $ lst) - -dropPendingPrekeys :: (MonadClient m) => UserId -> m () -dropPendingPrekeys uid = retry x5 (write Q.dropPendingPrekeys (params LocalQuorum (Identity uid))) - -setUserLegalHoldStatus :: (MonadClient m) => TeamId -> UserId -> UserLegalHoldStatus -> m () -setUserLegalHoldStatus tid uid status = - retry x5 (write Q.updateUserLegalHoldStatus (params LocalQuorum (status, tid, uid))) - -setTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () -setTeamLegalholdWhitelisted tid = - retry x5 (write Q.insertLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) - -unsetTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () -unsetTeamLegalholdWhitelisted tid = - retry x5 (write Q.removeLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) - -isTeamLegalholdWhitelisted :: FeatureDefaults LegalholdConfig -> TeamId -> Client Bool -isTeamLegalholdWhitelisted FeatureLegalHoldDisabledPermanently _ = pure False -isTeamLegalholdWhitelisted FeatureLegalHoldDisabledByDefault _ = pure False -isTeamLegalholdWhitelisted FeatureLegalHoldWhitelistTeamsAndImplicitConsent tid = - isJust <$> (runIdentity <$$> retry x5 (query1 Q.selectLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid)))) - --- | Copied unchanged from "Brig.Provider.API". Interpret a service certificate and extract --- key and fingerprint. (This only has to be in 'MonadIO' because the FFI in OpenSSL works --- like that.) --- --- FUTUREWORK: It would be nice to move (part of) this to ssl-util, but it has types from --- brig-types and types-common. -validateServiceKey :: (MonadIO m) => ServiceKeyPEM -> m (Maybe (ServiceKey, Fingerprint Rsa)) -validateServiceKey pem = - liftIO $ - readPublicKey >>= \pk -> - case SSL.toPublicKey =<< pk of - Nothing -> pure Nothing - Just pk' -> do - Just sha <- SSL.getDigestByName "SHA256" - let size = SSL.rsaSize (pk' :: SSL.RSAPubKey) - if size < minRsaKeySize - then pure Nothing - else do - fpr <- Fingerprint <$> SSL.rsaFingerprint sha pk' - let bits = fromIntegral size * 8 - let key = ServiceKey RsaServiceKey bits pem - pure (Just (key, fpr)) - where - readPublicKey = - handleAny - (const $ pure Nothing) - (SSL.readPublicKey (LC8.unpack (toByteString pem)) <&> Just) - minRsaKeySize :: Int - minRsaKeySize = 256 -- Bytes (= 2048 bits) diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 9b078d222bb..3f9ca75a5a7 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -16,7 +16,6 @@ -- with this program. If not, see . -- | Tables that are used in this module: --- - billing_team_member -- - clients -- - conversation_codes -- - custom_backend @@ -24,9 +23,7 @@ -- - legalhold_service -- - legalhold_whitelisted -- - team --- - team_admin -- - team_member --- - user_team -- update using: `rg -i -P '(?:update|from|into)\s+([A-Za-z0-9_]+)' -or '$1' --no-line-number services/galley/src/Galley/Cassandra/Queries.hs | sort | uniq` module Galley.Cassandra.Queries ( selectCustomBackend, @@ -48,48 +45,14 @@ module Galley.Cassandra.Queries dropPendingPrekeys, selectPendingPrekeys, updateUserLegalHoldStatus, - selectLegalHoldWhitelistedTeam, insertLegalHoldWhitelistedTeam, removeLegalHoldWhitelistedTeam, - insertTeam, - listBillingTeamMembers, - listTeamAdmins, - selectTeamName, - selectUserTeamsFrom, - selectUserTeams, - selectTeamMember, - insertTeamMember, - insertUserTeam, - insertBillingTeamMember, - insertTeamAdmin, - updatePermissions, - deleteBillingTeamMember, - deleteTeamAdmin, - deleteTeamMember, - deleteUserTeam, - selectTeam, - selectUserTeamsIn, - selectTeamMembers, - selectOneUserTeam, - selectTeamBindingWritetime, - selectTeamBinding, - markTeamDeleted, - deleteTeam, - updateTeamStatus, - updateTeamName, - updateTeamIcon, - updateTeamIconKey, - updateTeamSplashScreen, - selectTeamMembersFrom, - selectTeamMembers', ) where import Cassandra as C hiding (Value) -import Cassandra.Util (Writetime) import Data.Domain (Domain) import Data.Id -import Data.Json.Util import Data.LegalHold import Data.Misc import Data.Text.Lazy qualified as LT @@ -100,159 +63,9 @@ import Wire.API.Conversation.Code import Wire.API.Password (Password) import Wire.API.Provider import Wire.API.Provider.Service -import Wire.API.Routes.Internal.Galley.TeamsIntra -import Wire.API.Team -import Wire.API.Team.Permission import Wire.API.Team.SearchVisibility import Wire.API.User.Client.Prekey --- Teams -------------------------------------------------------------------- - -selectTeam :: PrepQuery R (Identity TeamId) (UserId, Text, Icon, Maybe Text, Bool, Maybe TeamStatus, Maybe (Writetime TeamStatus), Maybe TeamBinding, Maybe Icon) -selectTeam = "select creator, name, icon, icon_key, deleted, status, writetime(status), binding, splash_screen from team where team = ?" - -selectTeamName :: PrepQuery R (Identity TeamId) (Identity Text) -selectTeamName = "select name from team where team = ?" - -selectTeamBinding :: PrepQuery R (Identity TeamId) (Identity (Maybe TeamBinding)) -selectTeamBinding = "select binding from team where team = ?" - -selectTeamBindingWritetime :: PrepQuery R (Identity TeamId) (Identity (Maybe Int64)) -selectTeamBindingWritetime = "select writetime(binding) from team where team = ?" - -selectTeamMember :: - PrepQuery - R - (TeamId, UserId) - ( Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -selectTeamMember = "select perms, invited_by, invited_at, legalhold_status from team_member where team = ? and user = ?" - -selectTeamMembersBase :: (IsString a) => [String] -> a -selectTeamMembersBase conds = fromString $ selectFrom <> " where team = ?" <> whereClause <> " order by user" - where - selectFrom = "select user, perms, invited_by, invited_at, legalhold_status from team_member" - whereClause = concatMap (" and " <>) conds - --- | This query fetches **all** members of a team, it should always be paginated -selectTeamMembers :: - PrepQuery - R - (Identity TeamId) - ( UserId, - Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -selectTeamMembers = selectTeamMembersBase [] - -selectTeamMembersFrom :: - PrepQuery - R - (TeamId, UserId) - ( UserId, - Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -selectTeamMembersFrom = selectTeamMembersBase ["user > ?"] - -selectTeamMembers' :: - PrepQuery - R - (TeamId, [UserId]) - ( UserId, - Permissions, - Writetime Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -selectTeamMembers' = - [r| - select user, perms, writetime(perms), invited_by, invited_at, legalhold_status - from team_member - where team = ? and user in ? order by user - |] - -selectUserTeams :: PrepQuery R (Identity UserId) (Identity TeamId) -selectUserTeams = "select team from user_team where user = ? order by team" - -selectOneUserTeam :: PrepQuery R (Identity UserId) (Identity TeamId) -selectOneUserTeam = "select team from user_team where user = ? limit 1" - -selectUserTeamsIn :: PrepQuery R (UserId, [TeamId]) (Identity TeamId) -selectUserTeamsIn = "select team from user_team where user = ? and team in ? order by team" - -selectUserTeamsFrom :: PrepQuery R (UserId, TeamId) (Identity TeamId) -selectUserTeamsFrom = "select team from user_team where user = ? and team > ? order by team" - -insertTeam :: PrepQuery W (TeamId, UserId, Text, Icon, Maybe Text, TeamStatus, TeamBinding) () -insertTeam = "insert into team (team, creator, name, icon, icon_key, deleted, status, binding) values (?, ?, ?, ?, ?, false, ?, ?)" - -insertTeamMember :: PrepQuery W (TeamId, UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis) () -insertTeamMember = "insert into team_member (team, user, perms, invited_by, invited_at) values (?, ?, ?, ?, ?)" - -deleteTeamMember :: PrepQuery W (TeamId, UserId) () -deleteTeamMember = "delete from team_member where team = ? and user = ?" - -insertBillingTeamMember :: PrepQuery W (TeamId, UserId) () -insertBillingTeamMember = "insert into billing_team_member (team, user) values (?, ?)" - -deleteBillingTeamMember :: PrepQuery W (TeamId, UserId) () -deleteBillingTeamMember = "delete from billing_team_member where team = ? and user = ?" - -listBillingTeamMembers :: PrepQuery R (Identity TeamId) (Identity UserId) -listBillingTeamMembers = "select user from billing_team_member where team = ?" - -insertTeamAdmin :: PrepQuery W (TeamId, UserId) () -insertTeamAdmin = "insert into team_admin (team, user) values (?, ?)" - -deleteTeamAdmin :: PrepQuery W (TeamId, UserId) () -deleteTeamAdmin = "delete from team_admin where team = ? and user = ?" - -listTeamAdmins :: PrepQuery R (Identity TeamId) (Identity UserId) -listTeamAdmins = "select user from team_admin where team = ?" - --- | This is not an upsert, but we can't add `IF EXISTS` here, or cassandra will yell `Invalid --- "Batch with conditions cannot span multiple tables"` at us. So we make sure in the --- application logic to only call this if the user exists (in the handler, not entirely --- race-condition-proof, unfortunately). -updatePermissions :: PrepQuery W (Permissions, TeamId, UserId) () -updatePermissions = "update team_member set perms = ? where team = ? and user = ?" - -insertUserTeam :: PrepQuery W (UserId, TeamId) () -insertUserTeam = "insert into user_team (user, team) values (?, ?)" - -deleteUserTeam :: PrepQuery W (UserId, TeamId) () -deleteUserTeam = "delete from user_team where user = ? and team = ?" - -markTeamDeleted :: PrepQuery W (TeamStatus, TeamId) () -markTeamDeleted = {- `IF EXISTS`, but that requires benchmarking -} "update team set status = ? where team = ?" - -deleteTeam :: PrepQuery W (TeamStatus, TeamId) () -deleteTeam = {- `IF EXISTS`, but that requires benchmarking -} "update team using timestamp 32503680000000000 set name = 'default', icon = 'default', status = ? where team = ? " - -updateTeamName :: PrepQuery W (Text, TeamId) () -updateTeamName = {- `IF EXISTS`, but that requires benchmarking -} "update team set name = ? where team = ?" - -updateTeamIcon :: PrepQuery W (Text, TeamId) () -updateTeamIcon = {- `IF EXISTS`, but that requires benchmarking -} "update team set icon = ? where team = ?" - -updateTeamIconKey :: PrepQuery W (Text, TeamId) () -updateTeamIconKey = {- `IF EXISTS`, but that requires benchmarking -} "update team set icon_key = ? where team = ?" - -updateTeamStatus :: PrepQuery W (TeamStatus, TeamId) () -updateTeamStatus = {- `IF EXISTS`, but that requires benchmarking -} "update team set status = ? where team = ?" - -updateTeamSplashScreen :: PrepQuery W (Text, TeamId) () -updateTeamSplashScreen = {- `IF EXISTS`, but that requires benchmarking -} "update team set splash_screen = ? where team = ?" - -- Conversations accessible by code ----------------------------------------- insertCode :: PrepQuery W (Key, Value, ConvId, Scope, Maybe Password, Int32) () @@ -336,12 +149,6 @@ updateUserLegalHoldStatus = where team = ? and user = ? |] -selectLegalHoldWhitelistedTeam :: PrepQuery R (Identity TeamId) (Identity TeamId) -selectLegalHoldWhitelistedTeam = - [r| - select team from legalhold_whitelisted where team = ? - |] - insertLegalHoldWhitelistedTeam :: PrepQuery W (Identity TeamId) () insertLegalHoldWhitelistedTeam = [r| diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index 7a7296ff070..40513789326 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -16,8 +16,7 @@ -- with this program. If not, see . module Galley.Cassandra.Team - ( interpretTeamStoreToCassandra, - interpretTeamMemberStoreToCassandra, + ( interpretTeamMemberStoreToCassandra, interpretTeamListToCassandra, interpretInternalTeamListToCassandra, interpretTeamMemberStoreToCassandraWithPaging, @@ -25,142 +24,28 @@ module Galley.Cassandra.Team where import Cassandra -import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall)) import Control.Lens hiding ((<|)) import Control.Monad.Catch (throwM) import Control.Monad.Extra (ifM) -import Data.ByteString.Conversion (toByteString') -import Data.Id as Id -import Data.Json.Util (UTCTimeMillis (..), toUTCTimeMillis) +import Data.Id +import Data.Json.Util (UTCTimeMillis (..)) import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) -import Data.Map.Strict qualified as Map import Data.Range -import Data.Set qualified as Set -import Data.Text.Encoding -import Data.UUID.V4 (nextRandom) -import Galley.Aws qualified as Aws -import Galley.Cassandra.LegalHold (isTeamLegalholdWhitelisted) -import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store import Galley.Cassandra.Util import Galley.Effects.TeamMemberStore -import Galley.Effects.TeamStore (TeamStore (..)) -import Galley.Env -import Galley.Monad -import Galley.Options -import Galley.Types.Teams +import Galley.Types.Teams (FeatureDefaults (..)) import Imports hiding (Set, max) import Polysemy import Polysemy.Input import Polysemy.TinyLog -import UnliftIO qualified -import Wire.API.Routes.Internal.Galley.TeamsIntra -import Wire.API.Team import Wire.API.Team.Feature import Wire.API.Team.Member -import Wire.API.Team.Member.Info (TeamMemberInfo (TeamMemberInfo)) -import Wire.API.Team.Member.Info qualified as Info -import Wire.API.Team.Permission (Perm (SetBilling), Permissions, self) -import Wire.ConversationStore (ConversationStore) -import Wire.ConversationStore qualified as E +import Wire.API.Team.Permission (Permissions) import Wire.ListItems import Wire.Sem.Paging.Cassandra - -interpretTeamStoreToCassandra :: - ( Member (Embed IO) r, - Member (Input Env) r, - Member (Input ClientState) r, - Member TinyLog r, - Member ConversationStore r - ) => - FeatureDefaults LegalholdConfig -> - Sem (TeamStore ': r) a -> - Sem r a -interpretTeamStoreToCassandra lh = interpret $ \case - CreateTeamMember tid mem -> do - logEffect "TeamStore.CreateTeamMember" - embedClient (addTeamMember tid mem) - SetTeamMemberPermissions perm0 tid uid perm1 -> do - logEffect "TeamStore.SetTeamMemberPermissions" - embedClient (updateTeamMember perm0 tid uid perm1) - CreateTeam t uid n i k b -> do - logEffect "TeamStore.CreateTeam" - createTeam t uid n i k b - DeleteTeamMember tid uid -> do - logEffect "TeamStore.DeleteTeamMember" - embedClient (removeTeamMember tid uid) - GetBillingTeamMembers tid -> do - logEffect "TeamStore.GetBillingTeamMembers" - embedClient (listBillingTeamMembers tid) - GetTeamAdmins tid -> do - logEffect "TeamStore.GetTeamAdmins" - embedClient (listTeamAdmins tid) - GetTeam tid -> do - logEffect "TeamStore.GetTeam" - embedClient (team tid) - GetTeamName tid -> do - logEffect "TeamStore.GetTeamName" - embedClient (getTeamName tid) - SelectTeams uid tids -> do - logEffect "TeamStore.SelectTeams" - embedClient (teamIdsOf uid tids) - GetTeamMember tid uid -> do - logEffect "TeamStore.GetTeamMember" - embedClient (teamMember lh tid uid) - GetTeamMembersWithLimit tid n -> do - logEffect "TeamStore.GetTeamMembersWithLimit" - embedClient (teamMembersWithLimit lh tid n) - GetTeamMembers tid -> do - logEffect "TeamStore.GetTeamMembers" - embedClient (teamMembersCollectedWithPagination lh tid) - SelectTeamMembers tid uids -> do - logEffect "TeamStore.SelectTeamMembers" - embedClient (teamMembersLimited lh tid uids) - SelectTeamMemberInfos tid uids -> do - logEffect "TeamStore.SelectTeamMemberInfos" - embedClient (teamMemberInfos tid uids) - GetUserTeams uid -> do - logEffect "TeamStore.GetUserTeams" - embedClient (userTeams uid) - GetUsersTeams uids -> do - logEffect "TeamStore.GetUsersTeams" - embedClient (usersTeams uids) - GetOneUserTeam uid -> do - logEffect "TeamStore.GetOneUserTeam" - embedClient (oneUserTeam uid) - GetTeamsBindings tid -> do - logEffect "TeamStore.GetTeamsBindings" - embedClient (getTeamsBindings tid) - GetTeamBinding tid -> do - logEffect "TeamStore.GetTeamBinding" - embedClient (getTeamBinding tid) - GetTeamCreationTime tid -> do - logEffect "TeamStore.GetTeamCreationTime" - embedClient (teamCreationTime tid) - DeleteTeam tid -> do - logEffect "TeamStore.DeleteTeam" - deleteTeam tid - SetTeamData tid upd -> do - logEffect "TeamStore.SetTeamData" - embedClient (updateTeam tid upd) - SetTeamStatus tid st -> do - logEffect "TeamStore.SetTeamStatus" - embedClient (updateTeamStatus tid st) - FanoutLimit -> do - logEffect "TeamStore.FanoutLimit" - embedApp (currentFanoutLimit <$> view options) - GetLegalHoldFlag -> do - logEffect "TeamStore.GetLegalHoldFlag" - view (options . settings . featureFlags . to npProject) <$> input - EnqueueTeamEvent e -> do - logEffect "TeamStore.EnqueueTeamEvent" - menv <- inputs (view aEnv) - for_ menv $ \env -> - embed @IO (Aws.execute env (Aws.enqueue e)) - SelectTeamMembersPaginated tid uids mps lim -> do - logEffect "TeamStore.SelectTeamMembersPaginated" - embedClient (selectSomeTeamMembersPaginated lh tid uids mps lim) +import Wire.TeamStore.Cassandra.Queries qualified as Cql interpretTeamListToCassandra :: ( Member (Embed IO) r, @@ -220,41 +105,6 @@ interpretTeamMemberStoreToCassandraWithPaging lh = interpret $ \case logEffect "TeamMemberStore.ListTeamMembers" embedClient $ teamMembersPageFrom lh tid mps lim -createTeam :: - ( Member (Input ClientState) r, - Member (Embed IO) r - ) => - Maybe TeamId -> - UserId -> - Range 1 256 Text -> - Icon -> - Maybe (Range 1 256 Text) -> - TeamBinding -> - Sem r Team -createTeam t uid (fromRange -> n) i k b = do - tid <- embed @IO $ maybe (Id <$> liftIO nextRandom) pure t - - embedClient $ retry x5 $ write Cql.insertTeam (params LocalQuorum (tid, uid, n, i, fromRange <$> k, initialStatus b, b)) - pure (newTeam tid uid n i b & teamIconKey .~ (fromRange <$> k)) - where - initialStatus Binding = PendingActive -- Team becomes Active after User account activation - initialStatus NonBinding = Active - -listBillingTeamMembers :: TeamId -> Client [UserId] -listBillingTeamMembers tid = - fmap runIdentity - <$> retry x1 (query Cql.listBillingTeamMembers (params LocalQuorum (Identity tid))) - -listTeamAdmins :: TeamId -> Client [UserId] -listTeamAdmins tid = - fmap runIdentity - <$> retry x1 (query Cql.listTeamAdmins (params LocalQuorum (Identity tid))) - -getTeamName :: TeamId -> Client (Maybe Text) -getTeamName tid = - fmap runIdentity - <$> retry x1 (query1 Cql.selectTeamName (params LocalQuorum (Identity tid))) - teamIdsFrom :: UserId -> Maybe TeamId -> Range 1 100 Int32 -> Client (ResultSet TeamId) teamIdsFrom usr range (fromRange -> max) = mkResultSet . fmap runIdentity . strip <$> case range of @@ -269,228 +119,6 @@ teamIdsForPagination usr range (fromRange -> max) = Just c -> paginate Cql.selectUserTeamsFrom (paramsP LocalQuorum (usr, c) max) Nothing -> paginate Cql.selectUserTeams (paramsP LocalQuorum (Identity usr) max) -teamMember :: FeatureDefaults LegalholdConfig -> TeamId -> UserId -> Client (Maybe TeamMember) -teamMember lh t u = - newTeamMember'' u =<< retry x1 (query1 Cql.selectTeamMember (params LocalQuorum (t, u))) - where - newTeamMember'' :: - UserId -> - Maybe (Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -> - Client (Maybe TeamMember) - newTeamMember'' _ Nothing = pure Nothing - newTeamMember'' uid (Just (perms, minvu, minvt, mulhStatus)) = - Just <$> newTeamMember' lh t (uid, perms, minvu, minvt, mulhStatus) - -addTeamMember :: TeamId -> TeamMember -> Client () -addTeamMember t m = - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery - Cql.insertTeamMember - ( t, - m ^. userId, - m ^. permissions, - m ^? invitation . _Just . _1, - m ^? invitation . _Just . _2 - ) - addPrepQuery Cql.insertUserTeam (m ^. userId, t) - - when (m `hasPermission` SetBilling) $ - addPrepQuery Cql.insertBillingTeamMember (t, m ^. userId) - - when (isAdminOrOwner (m ^. permissions)) $ - addPrepQuery Cql.insertTeamAdmin (t, m ^. userId) - -updateTeamMember :: - -- | Old permissions, used for maintaining 'billing_team_member' and 'team_admin' tables - Permissions -> - TeamId -> - UserId -> - -- | New permissions - Permissions -> - Client () -updateTeamMember oldPerms tid uid newPerms = do - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery Cql.updatePermissions (newPerms, tid, uid) - - -- update billing_team_member table - let permDiff = Set.difference `on` self - acquiredPerms = newPerms `permDiff` oldPerms - lostPerms = oldPerms `permDiff` newPerms - - when (SetBilling `Set.member` acquiredPerms) $ - addPrepQuery Cql.insertBillingTeamMember (tid, uid) - when (SetBilling `Set.member` lostPerms) $ - addPrepQuery Cql.deleteBillingTeamMember (tid, uid) - - -- update team_admin table - let wasAdmin = isAdminOrOwner oldPerms - isAdmin = isAdminOrOwner newPerms - - when (isAdmin && not wasAdmin) $ - addPrepQuery Cql.insertTeamAdmin (tid, uid) - - when (not isAdmin && wasAdmin) $ - addPrepQuery Cql.deleteTeamAdmin (tid, uid) - -removeTeamMember :: TeamId -> UserId -> Client () -removeTeamMember t m = - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery Cql.deleteTeamMember (t, m) - addPrepQuery Cql.deleteUserTeam (m, t) - addPrepQuery Cql.deleteBillingTeamMember (t, m) - addPrepQuery Cql.deleteTeamAdmin (t, m) - -team :: TeamId -> Client (Maybe TeamData) -team tid = - fmap toTeam <$> retry x1 (query1 Cql.selectTeam (params LocalQuorum (Identity tid))) - where - toTeam (u, n, i, k, d, s, st, b, ss) = - let t = newTeam tid u n i (fromMaybe NonBinding b) & teamIconKey .~ k & teamSplashScreen .~ fromMaybe DefaultIcon ss - status = if d then PendingDelete else fromMaybe Active s - in TeamData t status (writetimeToUTC <$> st) - -teamIdsOf :: UserId -> [TeamId] -> Client [TeamId] -teamIdsOf usr tids = - map runIdentity <$> retry x1 (query Cql.selectUserTeamsIn (params LocalQuorum (usr, toList tids))) - -teamMembersWithLimit :: - FeatureDefaults LegalholdConfig -> - TeamId -> - Range 1 HardTruncationLimit Int32 -> - Client TeamMemberList -teamMembersWithLimit lh t (fromRange -> limit) = do - -- NOTE: We use +1 as size and then trim it due to the semantics of C* when getting a page with the exact same size - pageTuple <- retry x1 (paginate Cql.selectTeamMembers (paramsP LocalQuorum (Identity t) (limit + 1))) - ms <- mapM (newTeamMember' lh t) . take (fromIntegral limit) $ result pageTuple - pure $ - if hasMore pageTuple - then newTeamMemberList ms ListTruncated - else newTeamMemberList ms ListComplete - --- NOTE: Use this function with care... should only be required when deleting a team! --- Maybe should be left explicitly for the caller? -teamMembersCollectedWithPagination :: FeatureDefaults LegalholdConfig -> TeamId -> Client [TeamMember] -teamMembersCollectedWithPagination lh tid = do - mems <- teamMembersForPagination tid Nothing (unsafeRange 2000) - collectTeamMembersPaginated [] mems - where - collectTeamMembersPaginated acc mems = do - tMembers <- mapM (newTeamMember' lh tid) (result mems) - if hasMore mems - then collectTeamMembersPaginated (tMembers ++ acc) =<< nextPage mems - else pure (tMembers ++ acc) - --- Lookup only specific team members: this is particularly useful for large teams when --- needed to look up only a small subset of members (typically 2, user to perform the action --- and the target user) -teamMembersLimited :: FeatureDefaults LegalholdConfig -> TeamId -> [UserId] -> Client [TeamMember] -teamMembersLimited lh t u = - mapM (\(uid, perms, _, minvu, minvt, mlh) -> newTeamMember' lh t (uid, perms, minvu, minvt, mlh)) - =<< retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) - -teamMemberInfos :: TeamId -> [UserId] -> Client [TeamMemberInfo] -teamMemberInfos t u = - mkTeamMemberInfo - <$$> retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) - where - mkTeamMemberInfo :: (UserId, Permissions, Writetime Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -> TeamMemberInfo - mkTeamMemberInfo (uid, perms, permsWT, _, _, _) = - TeamMemberInfo - { Info.userId = uid, - Info.permissions = perms, - Info.permissionsWriteTime = toUTCTimeMillis $ writetimeToUTC permsWT - } - -userTeams :: UserId -> Client [TeamId] -userTeams u = - map runIdentity - <$> retry x1 (query Cql.selectUserTeams (params LocalQuorum (Identity u))) - -usersTeams :: [UserId] -> Client (Map UserId TeamId) -usersTeams uids = do - pairs :: [(UserId, TeamId)] <- - catMaybes - <$> UnliftIO.pooledMapConcurrentlyN 8 (\uid -> (uid,) <$$> oneUserTeam uid) uids - pure $ foldl' (\m (k, v) -> Map.insert k v m) Map.empty pairs - -oneUserTeam :: UserId -> Client (Maybe TeamId) -oneUserTeam u = - fmap runIdentity - <$> retry x1 (query1 Cql.selectOneUserTeam (params LocalQuorum (Identity u))) - -teamCreationTime :: TeamId -> Client (Maybe TeamCreationTime) -teamCreationTime t = - checkCreation . fmap runIdentity - <$> retry x1 (query1 Cql.selectTeamBindingWritetime (params LocalQuorum (Identity t))) - where - checkCreation (Just (Just ts)) = Just $ TeamCreationTime ts - checkCreation _ = Nothing - -getTeamBinding :: TeamId -> Client (Maybe TeamBinding) -getTeamBinding t = - fmap (fromMaybe NonBinding . runIdentity) - <$> retry x1 (query1 Cql.selectTeamBinding (params LocalQuorum (Identity t))) - -getTeamsBindings :: [TeamId] -> Client [TeamBinding] -getTeamsBindings = - fmap catMaybes - . UnliftIO.pooledMapConcurrentlyN 8 getTeamBinding - -deleteTeam :: - ( Member (Input ClientState) r, - Member (Embed IO) r, - Member ConversationStore r - ) => - TeamId -> - Sem r () -deleteTeam tid = do - embedClient (markTeamDeletedAndRemoveTeamMembers tid) - E.deleteTeamConversations tid - embedClient (retry x5 $ write Cql.deleteTeam (params LocalQuorum (Deleted, tid))) - -markTeamDeletedAndRemoveTeamMembers :: TeamId -> Client () -markTeamDeletedAndRemoveTeamMembers tid = do - -- TODO: delete service_whitelist records that mention this team - retry x5 $ write Cql.markTeamDeleted (params LocalQuorum (PendingDelete, tid)) - mems <- teamMembersForPagination tid Nothing (unsafeRange 2000) - removeTeamMembers mems - where - removeTeamMembers :: - Page - ( UserId, - Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -> - Client () - removeTeamMembers mems = do - mapM_ (removeTeamMember tid . view _1) (result mems) - unless (null $ result mems) $ - removeTeamMembers =<< liftClient (nextPage mems) - -updateTeamStatus :: TeamId -> TeamStatus -> Client () -updateTeamStatus t s = retry x5 $ write Cql.updateTeamStatus (params LocalQuorum (s, t)) - -updateTeam :: TeamId -> TeamUpdateData -> Client () -updateTeam tid u = retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - for_ (u ^. nameUpdate) $ \n -> - addPrepQuery Cql.updateTeamName (fromRange n, tid) - for_ (u ^. iconUpdate) $ \i -> - addPrepQuery Cql.updateTeamIcon (decodeUtf8 . toByteString' $ i, tid) - for_ (u ^. iconKeyUpdate) $ \k -> - addPrepQuery Cql.updateTeamIconKey (fromRange k, tid) - for_ (u ^. splashScreenUpdate) $ \ss -> - addPrepQuery Cql.updateTeamSplashScreen (decodeUtf8 . toByteString' $ ss, tid) - -- | Construct 'TeamMember' from database tuple. -- If FeatureLegalHoldWhitelistTeamsAndImplicitConsent is enabled set UserLegalHoldDisabled -- if team is whitelisted. @@ -526,6 +154,12 @@ newTeamMember' lh tid (uid, perms, minvu, minvt, fromMaybe defUserLegalHoldStatu mk Nothing Nothing = pure $ mkTeamMember uid perms Nothing lhStatus mk _ _ = throwM $ ErrorCall "TeamMember with incomplete metadata." +isTeamLegalholdWhitelisted :: FeatureDefaults LegalholdConfig -> TeamId -> Client Bool +isTeamLegalholdWhitelisted FeatureLegalHoldDisabledPermanently _ = pure False +isTeamLegalholdWhitelisted FeatureLegalHoldDisabledByDefault _ = pure False +isTeamLegalholdWhitelisted FeatureLegalHoldWhitelistTeamsAndImplicitConsent tid = + isJust <$> (runIdentity <$$> retry x5 (query1 Cql.selectLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid)))) + type RawTeamMember = (UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -- This function has a bit of a difficult type to work with because we don't @@ -548,18 +182,3 @@ teamMembersPageFrom lh tid pagingState (fromRange -> max) = do page <- paginateWithState Cql.selectTeamMembers (paramsPagingState LocalQuorum (Identity tid) max pagingState) members <- mapM (newTeamMember' lh tid) (pwsResults page) pure $ PageWithState members (pwsState page) - -selectSomeTeamMembersPaginated :: - FeatureDefaults LegalholdConfig -> - TeamId -> - [UserId] -> - Maybe PagingState -> - Range 1 HardTruncationLimit Int32 -> - Client (PageWithState TeamMember) -selectSomeTeamMembersPaginated lh tid uids pagingState (fromRange -> max) = do - page <- paginateWithState Cql.selectTeamMembers' (paramsPagingState LocalQuorum (tid, uids) max pagingState) - members <- mapM mkTm (pwsResults page) - pure $ PageWithState members (pwsState page) - where - mkTm (uid, perms, _, minvu, minvt, fromMaybe defUserLegalHoldStatus -> lhStatus) = - newTeamMember' lh tid (uid, perms, minvu, minvt, Just lhStatus) diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index cd4134e6f5b..b4fc14043e7 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -22,7 +22,7 @@ module Galley.Effects -- * Effects to access the Intra API BrigAPIAccess, FederatorAccess, - SparAccess, + SparAPIAccess, -- * External services ExternalAccess, @@ -66,19 +66,15 @@ import Galley.Effects.ClientStore import Galley.Effects.CodeStore import Galley.Effects.CustomBackendStore import Galley.Effects.FederatorAccess -import Galley.Effects.LegalHoldStore import Galley.Effects.ProposalStore import Galley.Effects.Queue import Galley.Effects.SearchVisibilityStore -import Galley.Effects.SparAccess import Galley.Effects.TeamFeatureStore import Galley.Effects.TeamMemberStore import Galley.Effects.TeamNotificationStore -import Galley.Effects.TeamStore import Galley.Env import Galley.Options import Galley.Types.Teams -import Imports import Polysemy import Polysemy.Error import Polysemy.Input @@ -93,6 +89,8 @@ import Wire.ExternalAccess import Wire.FireAndForget import Wire.GundeckAPIAccess import Wire.HashPassword +import Wire.LegalHoldStore +import Wire.LegalHoldStore.Env (LegalHoldEnv) import Wire.ListItems import Wire.NotificationSubsystem import Wire.RateLimit @@ -101,20 +99,23 @@ import Wire.Sem.Now import Wire.Sem.Paging.Cassandra import Wire.Sem.Random import Wire.ServiceStore +import Wire.SparAPIAccess import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore) import Wire.TeamCollaboratorsSubsystem (TeamCollaboratorsSubsystem) +import Wire.TeamJournal (TeamJournal) +import Wire.TeamStore import Wire.TeamSubsystem (TeamSubsystem) import Wire.UserGroupStore -- All the possible high-level effects. type GalleyEffects1 = - '[ SparAccess, - TeamCollaboratorsSubsystem, + '[ TeamCollaboratorsSubsystem, ConversationSubsystem, + TeamSubsystem, + SparAPIAccess, NotificationSubsystem, ExternalAccess, BrigAPIAccess, - TeamSubsystem, GundeckAPIAccess, Rpc, FederatorAccess, @@ -128,12 +129,14 @@ type GalleyEffects1 = HashPassword, Random, CustomBackendStore, - LegalHoldStore, SearchVisibilityStore, + TeamStore, + TeamJournal, + LegalHoldStore, + Input LegalHoldEnv, UserGroupStore, ServiceStore, TeamNotificationStore, - TeamStore, ConversationStore, MLSCommitLockStore, TeamFeatureStore, @@ -141,8 +144,9 @@ type GalleyEffects1 = TeamMemberStore CassandraPaging, ListItems LegacyPaging TeamId, ListItems InternalPaging TeamId, + Input FanoutLimit, Input AllTeamFeatures, - Input (Maybe [TeamId], FeatureDefaults LegalholdConfig), + Input (FeatureDefaults LegalholdConfig), Input (Local ()), Input Opts, Now, diff --git a/services/galley/src/Galley/Env.hs b/services/galley/src/Galley/Env.hs index 4a399af5345..f9b8400d930 100644 --- a/services/galley/src/Galley/Env.hs +++ b/services/galley/src/Galley/Env.hs @@ -26,7 +26,6 @@ import Data.Id import Data.Misc (HttpsUrl) import Data.Range import Data.Time.Clock.DiffTime (millisecondsToDiffTime) -import Galley.Aws qualified as Aws import Galley.Options import Galley.Options qualified as O import Galley.Queue qualified as Q @@ -39,6 +38,7 @@ import System.Logger import Util.Options import Wire.API.MLS.Keys import Wire.API.Team.Member +import Wire.AWS qualified as Aws import Wire.ExternalAccess.External import Wire.NotificationSubsystem.Interpreter import Wire.RateLimit.Interpreter (RateLimitEnv) @@ -46,6 +46,8 @@ import Wire.RateLimit.Interpreter (RateLimitEnv) data DeleteItem = TeamItem TeamId UserId (Maybe ConnId) deriving (Eq, Ord, Show) +type FanoutLimit = Range 1 HardTruncationLimit Int32 + -- | Main application environment. data Env = Env { _reqId :: RequestId, @@ -72,7 +74,7 @@ reqIdMsg :: RequestId -> Msg -> Msg reqIdMsg = ("request" .=) . unRequestId {-# INLINE reqIdMsg #-} -currentFanoutLimit :: Opts -> Range 1 HardTruncationLimit Int32 +currentFanoutLimit :: Opts -> FanoutLimit currentFanoutLimit o = do let optFanoutLimit = fromIntegral . fromRange $ fromMaybe defaultFanoutLimit (o ^. (O.settings . maxFanoutSize)) let maxSize = fromIntegral (o ^. (O.settings . maxTeamSize)) diff --git a/services/galley/src/Galley/External/LegalHoldService.hs b/services/galley/src/Galley/External/LegalHoldService.hs index af7adc0a637..0cb6e483038 100644 --- a/services/galley/src/Galley/External/LegalHoldService.hs +++ b/services/galley/src/Galley/External/LegalHoldService.hs @@ -29,7 +29,6 @@ where import Bilge qualified import Bilge.Response -import Brig.Types.Team.LegalHold import Control.Monad.Catch (MonadThrow (throwM)) import Data.Aeson import Data.ByteString.Char8 qualified as BS8 @@ -39,7 +38,6 @@ import Data.Id import Data.Misc import Data.Qualified (Local, QualifiedWithTag (tUntagged), tUnqualified) import Data.Set qualified as Set -import Galley.Effects.LegalHoldStore as LegalHoldData import Imports import Network.HTTP.Client qualified as Http import Network.HTTP.Types @@ -49,7 +47,9 @@ import System.Logger.Class qualified as Log import Wire.API.Error (ErrorS, throwS) import Wire.API.Error.Galley import Wire.API.Team.LegalHold.External +import Wire.API.Team.LegalHold.Internal import Wire.BrigAPIAccess +import Wire.LegalHoldStore as LegalHoldData ---------------------------------------------------------------------- -- api diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs deleted file mode 100644 index 502612df75e..00000000000 --- a/services/galley/src/Galley/Intra/Effects.hs +++ /dev/null @@ -1,43 +0,0 @@ --- 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 Galley.Intra.Effects where - -import Galley.Cassandra.Util -import Galley.Effects.SparAccess (SparAccess (..)) -import Galley.Env -import Galley.Intra.Spar -import Galley.Monad -import Imports -import Polysemy -import Polysemy.Input -import Polysemy.TinyLog - -interpretSparAccess :: - ( Member (Embed IO) r, - Member (Input Env) r, - Member TinyLog r - ) => - Sem (SparAccess ': r) a -> - Sem r a -interpretSparAccess = interpret $ \case - DeleteTeam tid -> do - logEffect "SparAccess.DeleteTeam" - embedApp $ deleteTeam tid - LookupScimUserInfo uid -> do - logEffect "SparAccess.LookupScimUserInfo" - embedApp $ lookupScimUserInfo uid diff --git a/services/galley/src/Galley/Intra/Spar.hs b/services/galley/src/Galley/Intra/Spar.hs deleted file mode 100644 index 3fede63dc16..00000000000 --- a/services/galley/src/Galley/Intra/Spar.hs +++ /dev/null @@ -1,48 +0,0 @@ --- 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 Galley.Intra.Spar - ( deleteTeam, - lookupScimUserInfo, - ) -where - -import Bilge -import Data.ByteString.Conversion -import Data.Id -import Galley.Intra.Util -import Galley.Monad -import Imports -import Network.HTTP.Types.Method -import Wire.API.User (ScimUserInfo) - --- | Notify Spar that a team is being deleted. -deleteTeam :: TeamId -> App () -deleteTeam tid = do - void . call Spar $ - method DELETE - . paths ["i", "teams", toByteString' tid] - . expect2xx - --- | Get the SCIM user info for a user. -lookupScimUserInfo :: UserId -> App ScimUserInfo -lookupScimUserInfo uid = do - response <- - call Spar $ - method POST - . paths ["i", "scim", "userinfo", toByteString' uid] - responseJsonError response diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 9536da440e9..ca34cc1212c 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -41,7 +41,6 @@ import Galley.API.Internal import Galley.API.Public.Servant import Galley.App import Galley.App qualified as App -import Galley.Aws (awsEnv) import Galley.Cassandra import Galley.Env import Galley.Monad @@ -67,6 +66,7 @@ import Wire.API.Routes.API import Wire.API.Routes.Public.Galley import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +import Wire.AWS (awsEnv) import Wire.OpenTelemetry (withTracerC) import Wire.PostgresMigrations (runAllMigrations) diff --git a/services/galley/src/Galley/TeamSubsystem.hs b/services/galley/src/Galley/TeamSubsystem.hs deleted file mode 100644 index 35320c83cb4..00000000000 --- a/services/galley/src/Galley/TeamSubsystem.hs +++ /dev/null @@ -1,46 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 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 Galley.TeamSubsystem where - -import Galley.Effects.TeamStore (TeamStore) -import Galley.Effects.TeamStore qualified as E -import Imports -import Polysemy -import Wire.API.Team.HardTruncationLimit -import Wire.API.Team.Member -import Wire.API.Team.Member.Info (TeamMemberInfoList (TeamMemberInfoList)) -import Wire.TeamSubsystem - --- This interpreter exists so galley code doesn't end up depending on --- GalleyAPIAccess, while it is possible to implement that, it'd add unnecesary --- HTTP calls for tiny things. --- --- When we actually implement TeamSubsystem this can move to wire-subsystem. --- Moving this to wire-subsystem before that would be too much work as the Store --- effects in galley are not as thin as we're doing them in wire-subsystems. --- They also depend on entire galley env. -interpretTeamSubsystem :: (Member TeamStore r) => InterpreterFor TeamSubsystem r -interpretTeamSubsystem = interpret $ \case - InternalGetTeamMember uid tid -> E.getTeamMember tid uid - InternalGetTeamMembers tid maxResults -> - E.getTeamMembersWithLimit tid $ fromMaybe hardTruncationLimitRange maxResults - InternalSelectTeamMemberInfos tid uids -> TeamMemberInfoList <$> E.selectTeamMemberInfos tid uids - InternalGetTeamAdmins tid -> do - admins <- E.getTeamAdmins tid - membs <- E.selectTeamMembers tid admins - pure $ newTeamMemberList membs ListComplete diff --git a/services/galley/test/integration/API/SQS.hs b/services/galley/test/integration/API/SQS.hs index ccf45732c90..9e8f35a6f1a 100644 --- a/services/galley/test/integration/API/SQS.hs +++ b/services/galley/test/integration/API/SQS.hs @@ -28,8 +28,7 @@ import Data.Id import Data.Set qualified as Set import Data.Text (pack) import Data.UUID qualified as UUID -import Galley.Aws qualified as Aws -import Galley.Options (JournalOpts) +import Galley.Options (JournalOpts, endpoint, queueName) import Imports import Network.HTTP.Client import Network.HTTP.Client.OpenSSL @@ -41,6 +40,7 @@ import System.Logger.Class qualified as L import Test.Tasty.HUnit import TestSetup import Util.Test.SQS qualified as SQS +import Wire.AWS qualified as Aws withTeamEventWatcher :: (HasCallStack) => (SQS.SQSWatcher TeamEvent -> TestM ()) -> TestM () withTeamEventWatcher action = do @@ -135,7 +135,7 @@ mkAWSEnv :: JournalOpts -> IO Aws.Env mkAWSEnv opts = do l <- L.new $ L.setOutput L.StdOut . L.setFormat Nothing $ L.defSettings -- TODO: use mkLogger'? mgr <- initHttpManager - Aws.mkEnv l mgr opts + Aws.mkEnv l mgr (opts ^. endpoint) (opts ^. queueName) decodeIdFromBS :: ByteString -> Id a decodeIdFromBS = Id . fromMaybe (error "failed to decode userId") . UUID.fromByteString . fromStrict diff --git a/services/galley/test/integration/API/Teams/LegalHold.hs b/services/galley/test/integration/API/Teams/LegalHold.hs index 72c2ee87686..f24abb8ada7 100644 --- a/services/galley/test/integration/API/Teams/LegalHold.hs +++ b/services/galley/test/integration/API/Teams/LegalHold.hs @@ -26,7 +26,6 @@ import API.Teams.LegalHold.Util import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert -import Brig.Types.Test.Arbitrary () import Control.Concurrent.Chan import Control.Lens hiding ((#)) import Data.Id @@ -34,7 +33,6 @@ import Data.LegalHold import Data.PEM import Data.Range import Data.Time.Clock qualified as Time -import Galley.Cassandra.LegalHold import Galley.Env qualified as Galley import Imports import Network.HTTP.Types.Status (status200, status404) @@ -53,6 +51,7 @@ import Wire.API.Team.Member import Wire.API.Team.Permission import Wire.API.Team.Role import Wire.API.User.Client +import Wire.LegalHoldStore.Cassandra tests :: IO TestSetup -> TestTree tests s = testGroup "Legalhold" [testsPublic s, testsInternal s] diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index 3f73ff3ffc3..94e15b9e542 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -30,7 +30,6 @@ import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert import Brig.Types.Intra (UserSet (..)) -import Brig.Types.Test.Arbitrary () import Control.Category ((>>>)) import Control.Concurrent.Chan import Control.Lens @@ -41,7 +40,6 @@ import Data.Map.Strict qualified as Map import Data.PEM import Data.Range import Data.Set qualified as Set -import Galley.Cassandra.LegalHold import Galley.Env qualified as Galley import Imports import Network.HTTP.Types.Status (status200, status404) @@ -62,6 +60,7 @@ import Wire.API.Team.Permission import Wire.API.Team.Role import Wire.API.User.Client import Wire.API.User.Client qualified as Client +import Wire.LegalHoldStore.Cassandra tests :: IO TestSetup -> TestTree tests s = diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index 0949ab9864b..b11611e842f 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -27,7 +27,6 @@ import API.SQS import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert -import Brig.Types.Test.Arbitrary () import Control.Concurrent.Async qualified as Async import Control.Concurrent.Chan import Control.Concurrent.Timeout hiding (threadDelay) diff --git a/services/galley/test/integration/Run.hs b/services/galley/test/integration/Run.hs index 54547697b7c..72eb745c1aa 100644 --- a/services/galley/test/integration/Run.hs +++ b/services/galley/test/integration/Run.hs @@ -34,7 +34,6 @@ import Data.Text (pack) import Data.Text.Encoding (encodeUtf8) import Data.Yaml (decodeFileEither) import Federation -import Galley.Aws qualified as Aws import Galley.Options hiding (endpoint) import Galley.Options qualified as O import Imports hiding (local) @@ -54,6 +53,7 @@ import Util.Options import Util.Options.Common import Util.Test import Util.Test.SQS qualified as SQS +import Wire.AWS qualified as Aws newtype ServiceConfigFile = ServiceConfigFile String deriving (Eq, Ord, Typeable) diff --git a/services/galley/test/integration/TestSetup.hs b/services/galley/test/integration/TestSetup.hs index b35dce1fe21..da26f53f0f4 100644 --- a/services/galley/test/integration/TestSetup.hs +++ b/services/galley/test/integration/TestSetup.hs @@ -55,7 +55,6 @@ import Data.ByteString.Conversion import Data.Domain import Data.Proxy import Data.Text qualified as Text -import Galley.Aws qualified as Aws import Galley.Options (Opts) import Imports import Network.HTTP.Client qualified as HTTP @@ -70,6 +69,7 @@ import Wire.API.Federation.API import Wire.API.Federation.Domain import Wire.API.Federation.Version import Wire.API.VersionInfo +import Wire.AWS qualified as Aws type GalleyR = Request -> Request diff --git a/services/spar/src/Spar/Scim/Types.hs b/services/spar/src/Spar/Scim/Types.hs index 5877b7884a2..b2b6b360af7 100644 --- a/services/spar/src/Spar/Scim/Types.hs +++ b/services/spar/src/Spar/Scim/Types.hs @@ -30,9 +30,9 @@ -- * Request and response types for SCIM-related endpoints. module Spar.Scim.Types where -import Brig.Types.Test.Arbitrary (Arbitrary (..)) import Control.Lens (view) import Imports +import Test.QuickCheck (Arbitrary (..)) import Test.QuickCheck.Gen (elements) import qualified Web.Scim.Schema.Common as Scim import qualified Web.Scim.Schema.User as Scim.User diff --git a/services/wire-server-enterprise b/services/wire-server-enterprise index 5260f0d5038..255cf1a0341 160000 --- a/services/wire-server-enterprise +++ b/services/wire-server-enterprise @@ -1 +1 @@ -Subproject commit 5260f0d5038441351e878afaaaaa38830db87c18 +Subproject commit 255cf1a034143ffd6a408012356ab350ede4cd97 From 3c6b260fbf58d2a2bcfe2ea16be845448e1c1760 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Mon, 1 Dec 2025 09:30:45 +0100 Subject: [PATCH 02/60] Explain MultiIngressSSO test helpers (#4882) Clarify test helpers' meanings: Explain a bit better what the test helpers in MultiIngressSSO are about. --- .../explain-MultiIngressSSO-test-helpers | 1 + integration/test/Test/Spar/MultiIngressSSO.hs | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 changelog.d/5-internal/explain-MultiIngressSSO-test-helpers diff --git a/changelog.d/5-internal/explain-MultiIngressSSO-test-helpers b/changelog.d/5-internal/explain-MultiIngressSSO-test-helpers new file mode 100644 index 00000000000..35f0bccb841 --- /dev/null +++ b/changelog.d/5-internal/explain-MultiIngressSSO-test-helpers @@ -0,0 +1 @@ +Explain MultiIngressSSO test helper functions a bit better. diff --git a/integration/test/Test/Spar/MultiIngressSSO.hs b/integration/test/Test/Spar/MultiIngressSSO.hs index 3df8c2e50f6..5a9ef87aef1 100644 --- a/integration/test/Test/Spar/MultiIngressSSO.hs +++ b/integration/test/Test/Spar/MultiIngressSSO.hs @@ -73,8 +73,8 @@ testMultiIngressSSO = do idpId <- asString $ idp.json %. "id" ernieEmail <- ("ernie@" <>) <$> randomDomain - checkMetadataSPIssuer domain ernieZHost tid - checkAuthnSPIssuer domain ernieZHost idpId tid + checkSPMetadata domain ernieZHost tid + checkAuthnRequest domain ernieZHost idpId tid finalizeLoginWithWrongZHost bertZHost ernieZHost domain tid ernieEmail (idpId, idpMeta) `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 @@ -89,8 +89,8 @@ testMultiIngressSSO = do makeSuccessfulSamlLogin domain ernieZHost tid ernieEmail idpId idpMeta bertEmail <- ("bert@" <>) <$> randomDomain - checkMetadataSPIssuer domain bertZHost tid - checkAuthnSPIssuer domain bertZHost idpId tid + checkSPMetadata domain bertZHost tid + checkAuthnRequest domain bertZHost idpId tid makeSuccessfulSamlLogin domain bertZHost tid bertEmail idpId idpMeta @@ -107,8 +107,11 @@ testMultiIngressSSO = do finalizeLoginWithWrongZHost bertZHost kermitZHost domain tid kermitEmail (idpId, idpMeta) `bindResponse` \resp -> do resp.status `shouldMatchInt` 404 -checkAuthnSPIssuer :: (HasCallStack) => String -> String -> String -> String -> App () -checkAuthnSPIssuer domain host idpId tid = +-- | Check the AuthnRequest by the SP (Wire backend) to be sent to the IdP +-- +-- Most important: The @Issuer@ must fit to the multi-ingress domain (@host@). +checkAuthnRequest :: (HasCallStack) => String -> String -> String -> String -> App () +checkAuthnRequest domain host idpId tid = initiateSamlLoginWithZHost domain (Just host) idpId `bindResponse` \authnreq -> do authnreq.status `shouldMatchInt` 200 @@ -133,8 +136,9 @@ checkAuthnSPIssuer domain host idpId tid = getIssuerUrl authnreq.body `shouldMatch` targetSPUrl -checkMetadataSPIssuer :: (HasCallStack) => String -> String -> String -> App () -checkMetadataSPIssuer domain host tid = +-- | Check the metadata of the ServiceProvider (i.e. of the Wire backend on multi-ingress domain @host@) +checkSPMetadata :: (HasCallStack) => String -> String -> String -> App () +checkSPMetadata domain host tid = getSPMetadataWithZHost domain (Just host) tid `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 From 91f576741f76ca66b3bc76a6cabc318e674c7aa9 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 2 Dec 2025 08:38:40 +0100 Subject: [PATCH 03/60] added description (#4691) --- .../src/Wire/API/Routes/Public/Brig/DomainVerification.hs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs index 03022fef789..c054e553061 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs @@ -459,6 +459,12 @@ type DomainVerificationAPI = :<|> Named "get-domain-registration" ( Summary "Get domain registration configuration by email" + :> Description + "- `due_to_existing_account`: boolean (optional, only present if `domain_redirect` is `no-registration`)\n\ + \- `backend`: object (optional, must be present if `domain_redirect` is `backend`)\n\ + \ - `config_url`: string (required)\n\ + \ - `webapp_url`: string (optional)\n\ + \- `sso_code`: string (optional, must be present if `domain_redirect` is `sso`)" :> From V10 :> CanThrow DomainVerificationInvalidDomain :> "get-domain-registration" From ff59a3ee753030bcf755694f2710bdc3eed03ccc Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Wed, 3 Dec 2025 11:23:45 +0100 Subject: [PATCH 04/60] Fix: brig always requires rabbitmq (#4886) * Always provide RabbitMQ settings in Brig's Helm chart Since 5866babe26f6b49511320dedb5b58a289ddcdbd4 RabbitMQ settings are mandatory for Brig. Before this commit they were only required if federation was enabled. * Provide RabbitMQ credentials in tests as well As RabbitMQ should be around anyways, it cannot hurt to be prepared to use it in integration tests. * Add changelog --- .../3-bug-fixes/provide-rabbitmq-for-brig-nonfederated | 4 ++++ charts/brig/templates/configmap.yaml | 2 +- charts/brig/templates/deployment.yaml | 2 -- charts/brig/templates/secret.yaml | 2 -- charts/brig/templates/tests/brig-integration.yaml | 2 -- charts/brig/values.yaml | 1 - 6 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3-bug-fixes/provide-rabbitmq-for-brig-nonfederated diff --git a/changelog.d/3-bug-fixes/provide-rabbitmq-for-brig-nonfederated b/changelog.d/3-bug-fixes/provide-rabbitmq-for-brig-nonfederated new file mode 100644 index 00000000000..3efd9944787 --- /dev/null +++ b/changelog.d/3-bug-fixes/provide-rabbitmq-for-brig-nonfederated @@ -0,0 +1,4 @@ +Since 5.23.23 (5866babe26f6b49511320dedb5b58a289ddcdbd4) RabbitMQ settings are +mandatory for Brig in both, federated and non-federated setups. Unfortunately, +this wasn't reflected in Brig's Helm chart. So, non-federated deployments were +failing. diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 7ea485cb94f..41c0e03f9dc 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -96,6 +96,7 @@ data: host: wire-server-enterprise port: 8080 {{- end }} + {{- end }} {{- with .rabbitmq }} rabbitmq: @@ -108,7 +109,6 @@ data: caCert: /etc/wire/brig/rabbitmq-ca/{{ .tlsCaSecretRef.key }} {{- end }} {{- end }} - {{- end }} {{- with .aws }} aws: diff --git a/charts/brig/templates/deployment.yaml b/charts/brig/templates/deployment.yaml index dd6cc972340..02ab76b8874 100644 --- a/charts/brig/templates/deployment.yaml +++ b/charts/brig/templates/deployment.yaml @@ -146,7 +146,6 @@ spec: value: {{ join "," .noProxyList | quote }} {{- end }} {{- end }} - {{- if .Values.config.enableFederation }} - name: RABBITMQ_USERNAME valueFrom: secretKeyRef: @@ -157,7 +156,6 @@ spec: secretKeyRef: name: brig key: rabbitmqPassword - {{- end }} ports: - containerPort: {{ .Values.service.internalPort }} startupProbe: diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index 400abdb4fdd..57543a97ab8 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -32,10 +32,8 @@ data: {{- if .oauthJwkKeyPair }} oauth_ed25519.jwk: {{ .oauthJwkKeyPair | b64enc | quote }} {{- end }} - {{- if $.Values.config.enableFederation }} rabbitmqUsername: {{ .rabbitmq.username | b64enc | quote }} rabbitmqPassword: {{ .rabbitmq.password | b64enc | quote }} - {{- end }} {{- if .elasticsearch }} elasticsearch-credentials.yaml: {{ .elasticsearch | toYaml | b64enc }} {{- end }} diff --git a/charts/brig/templates/tests/brig-integration.yaml b/charts/brig/templates/tests/brig-integration.yaml index 15996698ba8..433ded2177a 100644 --- a/charts/brig/templates/tests/brig-integration.yaml +++ b/charts/brig/templates/tests/brig-integration.yaml @@ -137,12 +137,10 @@ spec: value: "dummy" - name: AWS_REGION value: "eu-west-1" - {{- if .Values.config.enableFederation }} - name: RABBITMQ_USERNAME value: "guest" - name: RABBITMQ_PASSWORD value: "guest" - {{- end }} - name: TEST_XML value: /tmp/result.xml {{- if .Values.tests.config.uploadXml }} diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index efd57f73efa..63d65a96103 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -68,7 +68,6 @@ config: multiSFT: enabled: false # keep multiSFT default in sync with sft chart's multiSFT.enabled enableFederation: false # keep in sync with background-worker, cargohold and galley charts' config.enableFederation as well as wire-server chart's tags.federation - # Not used if enableFederation is false rabbitmq: host: rabbitmq port: 5672 From 0dfd9589433055aac366c6aeab7c3a58b3cfdb3a Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 4 Dec 2025 08:52:11 +0100 Subject: [PATCH 05/60] Skip group info mismatch error for broken groups (#4883) * Add utility to get group info * Check existing group before throwing mismatch error * Fix equality in group info check * Test previously broken group * Refactor existing mismatch logic * Add CHANGELOG entry * Lint --- .../mls-skip-error-for-broken-groups | 1 + integration/test/Test/MLS.hs | 41 ++++++++++++ .../src/Wire/ConversationStore.hs | 7 ++ .../src/Galley/API/MLS/GroupInfoCheck.hs | 65 +++++++++++++------ services/galley/src/Galley/API/MLS/Message.hs | 4 +- 5 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 changelog.d/2-features/mls-skip-error-for-broken-groups diff --git a/changelog.d/2-features/mls-skip-error-for-broken-groups b/changelog.d/2-features/mls-skip-error-for-broken-groups new file mode 100644 index 00000000000..629198a71e2 --- /dev/null +++ b/changelog.d/2-features/mls-skip-error-for-broken-groups @@ -0,0 +1 @@ +Commits with a broken group info are now let through if the group was already broken diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 26aead3de04..3d76fd7ff39 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -21,6 +21,7 @@ module Test.MLS where import API.Brig (claimKeyPackages, deleteClient) import API.Galley +import qualified API.GalleyInternal as I import Data.Bits import qualified Data.ByteString as B import qualified Data.ByteString.Base64 as Base64 @@ -1115,6 +1116,46 @@ testGroupInfoCheckDisabled = do $ \resp -> do resp.status `shouldMatchInt` 201 +testGroupInfoAlreadyBroken :: (HasCallStack) => App () +testGroupInfoAlreadyBroken = do + withModifiedBackend + ( def + { galleyCfg = + setField "settings.checkGroupInfo" True + } + ) + $ \domain -> do + (alice, tid, [bob, charlie, dee]) <- createTeam domain 4 + [alice1, bob1, charlie1, dee1] <- traverse (createMLSClient def) [alice, bob, charlie, dee] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1, dee1] + + conv <- postConversation alice1 defMLS {team = Just tid} >>= getJSON 201 + convId <- objConvId conv + createGroup def alice1 convId + + -- add bob normally + mp1 <- createAddCommit alice1 convId [bob] + void $ sendAndConsumeCommitBundle mp1 + + -- make a commit with an old group info + mp2 <- createAddCommit alice1 convId [charlie] + void $ sendAndConsumeCommitBundle mp2 {groupInfo = mp1.groupInfo} + + -- enable feature + do + I.setTeamFeatureLockStatus alice tid "mls" "unlocked" + mls <- + defAllFeatures + %. "mls.config" + >>= setField "groupInfoDiagnostics" True + let feat = object ["status" .= "enabled", "config" .= mls] + void $ setTeamFeatureConfig alice tid "mls" feat >>= getJSON 200 + + -- make another commit with an old group info + -- the group was already broken previously, so this should be accepted + mp3 <- createAddCommit alice1 convId [dee] + void $ sendAndConsumeCommitBundle mp3 {groupInfo = mp1.groupInfo} + testAddUsersDirectlyShouldFail :: (HasCallStack) => App () testAddUsersDirectlyShouldFail = do [alice, bob] <- replicateM 2 $ randomUser OwnDomain def diff --git a/libs/wire-subsystems/src/Wire/ConversationStore.hs b/libs/wire-subsystems/src/Wire/ConversationStore.hs index 56f319982db..2a529d40cd5 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore.hs @@ -231,3 +231,10 @@ data PostgresMigrationOpts = PostgresMigrationOpts instance FromJSON PostgresMigrationOpts where parseJSON = withObject "PostgresMigrationOpts" $ \o -> PostgresMigrationOpts <$> o .: "conversation" + +getConvOrSubGroupInfo :: + (Member ConversationStore r) => + ConvOrSubConvId -> + Sem r (Maybe GroupInfoData) +getConvOrSubGroupInfo (Conv c) = getGroupInfo c +getConvOrSubGroupInfo (SubConv c s) = getSubConversationGroupInfo c s diff --git a/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs b/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs index cb2f7f03f08..e4dcfc4e5bc 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs @@ -22,6 +22,7 @@ module Galley.API.MLS.GroupInfoCheck where import Control.Lens (view) +import Data.Bifunctor import Data.Id import Galley.API.Teams.Features.Get import Galley.Effects @@ -31,6 +32,8 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.NonDet +import Wire.API.Conversation hiding (Member) +import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MLS.Credential import Wire.API.MLS.Extension @@ -38,41 +41,65 @@ import Wire.API.MLS.GroupInfo import Wire.API.MLS.KeyPackage import Wire.API.MLS.LeafNode import Wire.API.MLS.RatchetTree +import Wire.API.MLS.Serialisation import Wire.API.Team.Feature +import Wire.ConversationStore import Wire.ConversationStore.MLS.Types data GroupInfoMismatch = GroupInfoMismatch {clients :: [(Int, ClientIdentity)]} + deriving (Show) checkGroupState :: forall r. ( Member (Error GroupInfoMismatch) r, Member (Input Opts) r, Member (Error MLSProtocolError) r, - Member TeamFeatureStore r + Member TeamFeatureStore r, + Member ConversationStore r ) => - Maybe TeamId -> + ConvOrSubConv -> IndexMap -> GroupInfo -> Sem r () -checkGroupState mTid leaves groupInfo = do - check <- isGroupInfoCheckEnabled mTid - when check $ do - trees <- - either - (\_ -> throw (mlsProtocolError "Could not parse ratchet tree extension in GroupInfo")) - pure - $ findExtension groupInfo.tbs.extensions - tree :: RatchetTree <- case trees of - (tree : _) -> pure tree - _ -> throw $ mlsProtocolError "No ratchet tree extension found in GroupInfo" - giLeaves <- imFromList <$> traverse (traverse getIdentity) (ratchetTreeLeaves tree) - when (leaves /= giLeaves) $ throw (GroupInfoMismatch (imAssocs leaves)) +checkGroupState convOrSub newLeaves groupInfo = do + check <- isGroupInfoCheckEnabled convOrSub.conv.mcMetadata.cnvmTeam + case (check, groupStateMismatch newLeaves groupInfo) of + (True, Left e) -> throw (mlsProtocolError e) + (True, Right (Just mismatch)) -> do + existingMismatch <- existingGroupStateMismatch convOrSub + when (isNothing existingMismatch) $ throw mismatch + _ -> pure () + +groupStateMismatch :: IndexMap -> GroupInfo -> Either Text (Maybe GroupInfoMismatch) +groupStateMismatch leaves groupInfo = do + trees <- + first + (const "Could not parse ratchet tree extension in GroupInfo") + $ findExtension groupInfo.tbs.extensions + tree :: RatchetTree <- case trees of + (tree : _) -> pure tree + _ -> Left "No ratchet tree extension found in GroupInfo" + giLeaves <- imFromList <$> traverse (traverse getIdentity) (ratchetTreeLeaves tree) + pure $ guard (leaves /= giLeaves) $> GroupInfoMismatch (imAssocs leaves) where - getIdentity :: LeafNode -> Sem r ClientIdentity - getIdentity leaf = case credentialIdentityAndKey leaf.credential of - Left e -> throw (mlsProtocolError e) - Right (cid, _) -> pure cid + getIdentity :: LeafNode -> Either Text ClientIdentity + getIdentity leaf = fst <$> credentialIdentityAndKey leaf.credential + +existingGroupStateMismatch :: + (Member ConversationStore r) => + ConvOrSubConv -> + Sem r (Maybe GroupInfoMismatch) +existingGroupStateMismatch convOrSub = + fmap join . runErrorS @MLSMissingGroupInfo $ + do + groupInfoData <- getConvOrSubGroupInfo convOrSub.id >>= noteS @MLSMissingGroupInfo + groupInfo <- + either (\_ -> throwS @MLSMissingGroupInfo) pure $ + decodeMLS' (unGroupInfoData groupInfoData) + case groupStateMismatch convOrSub.indexMap groupInfo of + Left _ -> throwS @MLSMissingGroupInfo + Right m -> pure m isGroupInfoCheckEnabled :: ( Member TeamFeatureStore r, diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 75abbecc6a2..ae77df7a522 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -313,7 +313,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do checkConversationOutOfSync newUsers lConvOrSub ciphersuite lift $ - checkGroupState convOrSub.conv.mcMetadata.cnvmTeam newIndexMap bundle.groupInfo.value + checkGroupState convOrSub newIndexMap bundle.groupInfo.value -- process additions and removals events <- @@ -332,7 +332,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do pure (events, newClients) Nothing -> do (newIndexMap, action) <- lift $ getExternalCommitData senderIdentity.client lConvOrSub bundle.epoch bundle.commit.value - lift $ checkGroupState convOrSub.conv.mcMetadata.cnvmTeam newIndexMap bundle.groupInfo.value + lift $ checkGroupState convOrSub newIndexMap bundle.groupInfo.value let senderIdentity' = senderIdentity {index = Just action.add} processExternalCommit senderIdentity' From d6edef799079ca8887229094b229cf10dbb658a2 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 4 Dec 2025 09:02:14 +0100 Subject: [PATCH 06/60] WPB-21591 Move dependencies (#4881) - Renamed FederatorAccess to FederationAPIAccess and moved it to wire-subsystems - Moved ProposalStore from Galley.Effects.ProposalStore to Wire.ProposalStore - Introduced ConversationSubsystemConfig to consolidate configuration dependencies previously accessed via Input Opts and Input Env --- .../Wire/ConversationSubsystem/Interpreter.hs | 11 ++ .../src/Wire/FederationAPIAccess.hs | 31 +++-- .../Wire/FederationAPIAccess/Interpreter.hs | 20 +-- .../src/Wire}/ProposalStore.hs | 2 +- .../src/Wire/ProposalStore/Cassandra.hs | 31 ++--- .../test/unit/Wire/MiniBackend.hs | 4 +- libs/wire-subsystems/wire-subsystems.cabal | 2 + services/galley/default.nix | 3 +- services/galley/galley.cabal | 8 +- services/galley/src/Galley/API/Action.hs | 52 ++++--- services/galley/src/Galley/API/Action/Kick.hs | 4 +- .../galley/src/Galley/API/Action/Leave.hs | 4 +- .../galley/src/Galley/API/Action/Reset.hs | 16 ++- services/galley/src/Galley/API/Clients.hs | 4 +- services/galley/src/Galley/API/Create.hs | 39 +++--- services/galley/src/Galley/API/Federation.hs | 33 +++-- services/galley/src/Galley/API/Internal.hs | 4 +- services/galley/src/Galley/API/LegalHold.hs | 47 ++++--- .../galley/src/Galley/API/MLS/CheckClients.hs | 5 +- .../galley/src/Galley/API/MLS/Commit/Core.hs | 17 ++- .../Galley/API/MLS/Commit/InternalCommit.hs | 19 ++- .../galley/src/Galley/API/MLS/GroupInfo.hs | 7 +- services/galley/src/Galley/API/MLS/Keys.hs | 7 +- services/galley/src/Galley/API/MLS/Message.hs | 15 +- .../galley/src/Galley/API/MLS/Migration.hs | 8 +- .../galley/src/Galley/API/MLS/OutOfSync.hs | 5 +- .../galley/src/Galley/API/MLS/Proposal.hs | 6 +- services/galley/src/Galley/API/MLS/Removal.hs | 16 +-- services/galley/src/Galley/API/MLS/Reset.hs | 9 +- .../src/Galley/API/MLS/SubConversation.hs | 25 ++-- services/galley/src/Galley/API/MLS/Util.hs | 2 +- services/galley/src/Galley/API/MLS/Welcome.hs | 7 +- services/galley/src/Galley/API/Message.hs | 11 +- services/galley/src/Galley/API/Query.hs | 30 ++-- services/galley/src/Galley/API/Teams.hs | 7 +- .../galley/src/Galley/API/Teams/Features.hs | 7 +- services/galley/src/Galley/API/Update.hs | 130 ++++++++++-------- services/galley/src/Galley/API/Util.hs | 33 ++--- services/galley/src/Galley/App.hs | 30 +++- services/galley/src/Galley/Effects.hs | 9 +- .../src/Galley/Effects/FederatorAccess.hs | 72 ---------- services/galley/src/Galley/Intra/Federator.hs | 121 ---------------- 42 files changed, 432 insertions(+), 481 deletions(-) rename {services/galley/src/Galley/Effects => libs/wire-subsystems/src/Wire}/ProposalStore.hs (97%) rename services/galley/src/Galley/Cassandra/Proposal.hs => libs/wire-subsystems/src/Wire/ProposalStore/Cassandra.hs (75%) delete mode 100644 services/galley/src/Galley/Effects/FederatorAccess.hs delete mode 100644 services/galley/src/Galley/Intra/Federator.hs diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs index 0cfd6d8bc82..089e6d14c76 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs @@ -22,6 +22,7 @@ import Data.Id import Data.Json.Util (ToJSONObject (toJSONObject)) import Data.Qualified import Data.Singletons (Sing) +import Galley.Types.Teams (FeatureDefaults) import Imports import Network.AMQP qualified as Q import Polysemy @@ -29,10 +30,13 @@ import Polysemy.Error import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action import Wire.API.Conversation.CellsState (CellsState (..)) +import Wire.API.Conversation.Protocol (ProtocolTag) import Wire.API.Event.Conversation import Wire.API.Federation.API (makeConversationUpdateBundle, sendBundle) import Wire.API.Federation.API.Galley.Notifications (ConversationUpdate (..)) import Wire.API.Federation.Error (FederationError) +import Wire.API.MLS.Keys (MLSKeysByPurpose, MLSPrivateKeys) +import Wire.API.Team.Feature (LegalholdConfig) import Wire.BackendNotificationQueueAccess (BackendNotificationQueueAccess, enqueueNotificationsConcurrently) import Wire.ConversationSubsystem import Wire.ExternalAccess (ExternalAccess, deliverAsync) @@ -41,6 +45,13 @@ import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation +data ConversationSubsystemConfig = ConversationSubsystemConfig + { mlsKeys :: Maybe (MLSKeysByPurpose MLSPrivateKeys), + federationProtocols :: Maybe [ProtocolTag], + legalholdDefaults :: FeatureDefaults LegalholdConfig, + maxConvSize :: Word16 + } + interpretConversationSubsystem :: ( Member (Error FederationError) r, Member BackendNotificationQueueAccess r, diff --git a/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs b/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs index 673e7f1d611..4d457295ec5 100644 --- a/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs @@ -33,20 +33,16 @@ data FederationAPIAccess (fedM :: Component -> Type -> Type) m a where Remote x -> fedM c a -> FederationAPIAccess fedM m (Either FederationError a) - RunFederatedConcurrently :: - forall (c :: Component) f a m x fedM. - (KnownComponent c, Foldable f) => - f (Remote x) -> - (Remote x -> fedM c a) -> - FederationAPIAccess fedM m [Either (Remote x, FederationError) (Remote a)] - -- | An action similar to 'RunFederatedConcurrently', but the input is - -- bucketed by domain before the RPCs are sent to the remote backends. - RunFederatedBucketed :: - forall (c :: Component) f a m x fedM. + RunFederatedConcurrentlyEither :: (KnownComponent c, Foldable f, Functor f) => f (Remote x) -> (Remote [x] -> fedM c a) -> FederationAPIAccess fedM m [Either (Remote [x], FederationError) (Remote a)] + RunFederatedConcurrentlyBucketsEither :: + (KnownComponent c, Foldable f) => + f (Remote x) -> + (Remote x -> fedM c a) -> + FederationAPIAccess fedM m [Either (Remote x, FederationError) (Remote a)] IsFederationConfigured :: FederationAPIAccess fedM m Bool makeSem ''FederationAPIAccess @@ -61,3 +57,18 @@ runFederated :: fedM c a -> Sem r a runFederated rx c = runFederatedEither rx c >>= fromEither + +runFederatedConcurrently :: + forall c fedM f x a r. + ( Member (FederationAPIAccess fedM) r, + Member (Error FederationError) r, + KnownComponent c, + Foldable f, + Functor f + ) => + f (Remote x) -> + (Remote [x] -> fedM c a) -> + Sem r [Remote a] +runFederatedConcurrently rx c = do + results <- runFederatedConcurrentlyEither rx c + fromEither $ mapLeft snd $ sequence results diff --git a/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs b/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs index 737cee6529e..47e1030779b 100644 --- a/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs @@ -81,8 +81,8 @@ interpretFederationAPIAccessGeneral runFedM isFederationConfigured = interpret $ \case RunFederatedEither remote rpc -> runFederatedEither runFedM remote rpc - RunFederatedConcurrently remotes rpc -> runFederatedConcurrently runFedM remotes rpc - RunFederatedBucketed remotes rpc -> runFederatedBucketed runFedM remotes rpc + RunFederatedConcurrentlyEither remotes rpc -> runFederatedConcurrently runFedM remotes rpc + RunFederatedConcurrentlyBucketsEither remotes rpc -> runFederatedBucketed runFedM remotes rpc IsFederationConfigured -> isFederationConfigured runFederatedEither :: @@ -95,25 +95,25 @@ runFederatedEither runFedM (tDomain -> remoteDomain) rpc = runFederatedConcurrently :: ( Foldable f, - Member (Concurrency 'Unsafe) r + Member (Concurrency 'Unsafe) r, + Functor f ) => FederatedActionRunner fedM r -> f (Remote a) -> - (Remote a -> fedM c b) -> - Sem r [Either (Remote a, FederationError) (Remote b)] + (Remote [a] -> fedM c b) -> + Sem r [Either (Remote [a], FederationError) (Remote b)] runFederatedConcurrently runFedM xs rpc = - unsafePooledForConcurrentlyN 8 (toList xs) $ \r -> + unsafePooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> bimap (r,) (qualifyAs r) <$> runFederatedEither runFedM r (rpc r) runFederatedBucketed :: ( Foldable f, - Functor f, Member (Concurrency 'Unsafe) r ) => FederatedActionRunner fedM r -> f (Remote a) -> - (Remote [a] -> fedM c b) -> - Sem r [Either (Remote [a], FederationError) (Remote b)] + (Remote a -> fedM c b) -> + Sem r [Either (Remote a, FederationError) (Remote b)] runFederatedBucketed runFedM xs rpc = - unsafePooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> + unsafePooledForConcurrentlyN 8 (toList xs) $ \r -> bimap (r,) (qualifyAs r) <$> runFederatedEither runFedM r (rpc r) diff --git a/services/galley/src/Galley/Effects/ProposalStore.hs b/libs/wire-subsystems/src/Wire/ProposalStore.hs similarity index 97% rename from services/galley/src/Galley/Effects/ProposalStore.hs rename to libs/wire-subsystems/src/Wire/ProposalStore.hs index cf549d576c3..130a2b2a0af 100644 --- a/services/galley/src/Galley/Effects/ProposalStore.hs +++ b/libs/wire-subsystems/src/Wire/ProposalStore.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 Galley.Effects.ProposalStore where +module Wire.ProposalStore where import Imports import Polysemy diff --git a/services/galley/src/Galley/Cassandra/Proposal.hs b/libs/wire-subsystems/src/Wire/ProposalStore/Cassandra.hs similarity index 75% rename from services/galley/src/Galley/Cassandra/Proposal.hs rename to libs/wire-subsystems/src/Wire/ProposalStore/Cassandra.hs index c7675b6be32..a2c971f820f 100644 --- a/services/galley/src/Galley/Cassandra/Proposal.hs +++ b/libs/wire-subsystems/src/Wire/ProposalStore/Cassandra.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 Galley.Cassandra.Proposal +module Wire.ProposalStore.Cassandra ( interpretProposalStoreToCassandra, ProposalOrigin (..), ) @@ -23,18 +23,16 @@ where import Cassandra import Data.Timeout -import Galley.Cassandra.Store -import Galley.Cassandra.Util -import Galley.Effects.ProposalStore import Imports import Polysemy import Polysemy.Input -import Polysemy.TinyLog import Wire.API.MLS.Epoch import Wire.API.MLS.Group import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.ConversationStore.Cassandra.Instances () +import Wire.ProposalStore +import Wire.Util (embedClient) -- | Proposals in the database expire after this timeout defaultTTL :: Timeout @@ -42,28 +40,27 @@ defaultTTL = 28 # Day interpretProposalStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r, - Member TinyLog r + Member (Input ClientState) r ) => Sem (ProposalStore ': r) a -> Sem r a interpretProposalStoreToCassandra = interpret $ \case StoreProposal groupId epoch ref origin raw -> do - logEffect "ProposalStore.StoreProposal" - embedClient . retry x5 $ + client <- input + embedClient client . retry x5 $ write (storeQuery defaultTTL) (params LocalQuorum (groupId, epoch, ref, origin, raw)) GetProposal groupId epoch ref -> do - logEffect "ProposalStore.GetProposal" - embedClient (runIdentity <$$> retry x1 (query1 getQuery (params LocalQuorum (groupId, epoch, ref)))) + client <- input + embedClient client (runIdentity <$$> retry x1 (query1 getQuery (params LocalQuorum (groupId, epoch, ref)))) GetAllPendingProposalRefs groupId epoch -> do - logEffect "ProposalStore.GetAllPendingProposalRefs" - embedClient (runIdentity <$$> retry x1 (query getAllPendingRef (params LocalQuorum (groupId, epoch)))) + client <- input + embedClient client (runIdentity <$$> retry x1 (query getAllPendingRef (params LocalQuorum (groupId, epoch)))) GetAllPendingProposals groupId epoch -> do - logEffect "ProposalStore.GetAllPendingProposals" - embedClient $ retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) + client <- input + embedClient client $ retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) DeleteAllProposals groupId -> do - logEffect "ProposalStore.DeleteAllProposals" - embedClient $ retry x5 (write deleteAllProposalsForGroup (params LocalQuorum (Identity groupId))) + client <- input + embedClient client $ retry x5 (write deleteAllProposalsForGroup (params LocalQuorum (Identity groupId))) storeQuery :: Timeout -> PrepQuery W (GroupId, Epoch, ProposalRef, ProposalOrigin, RawMLS Proposal) () storeQuery ttl = diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index a0d20c10702..aa521765bf0 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -739,6 +739,6 @@ miniFederationAPIAccess online = do if isJust (M.lookup (qDomain $ tUntagged remote) online) then FI.runFederatedEither runner remote rpc else pure $ Left do FederationUnexpectedError "RunFederatedEither" - RunFederatedConcurrently _remotes _rpc -> error "unimplemented: RunFederatedConcurrently" - RunFederatedBucketed _domain _rpc -> error "unimplemented: RunFederatedBucketed" + RunFederatedConcurrentlyEither _remotes _rpc -> error "unimplemented: RunFederatedConcurrently" + RunFederatedConcurrentlyBucketsEither _domain _rpc -> error "unimplemented: RunFederatedBucketed" IsFederationConfigured -> pure True diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 13c3773a0db..2d4cef48c64 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -286,6 +286,8 @@ library Wire.PropertyStore.Cassandra Wire.PropertySubsystem Wire.PropertySubsystem.Interpreter + Wire.ProposalStore + Wire.ProposalStore.Cassandra Wire.RateLimit Wire.RateLimit.Interpreter Wire.Rpc diff --git a/services/galley/default.nix b/services/galley/default.nix index 1f6558900e0..a6dbd9161ea 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -66,6 +66,7 @@ , pem , polysemy , polysemy-conc +, polysemy-plugin , polysemy-wire-zoo , process , prometheus-client @@ -148,7 +149,6 @@ mkDerivation { crypton crypton-x509 data-default - data-timeout errors exceptions extended @@ -173,6 +173,7 @@ mkDerivation { pem polysemy polysemy-conc + polysemy-plugin polysemy-wire-zoo prometheus-client raw-strings-qq diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 69a7cf3f29d..59bfdc5f121 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -140,7 +140,6 @@ library Galley.Cassandra.Client Galley.Cassandra.Code Galley.Cassandra.CustomBackend - Galley.Cassandra.Proposal Galley.Cassandra.Queries Galley.Cassandra.SearchVisibility Galley.Cassandra.Store @@ -155,8 +154,6 @@ library Galley.Effects.ClientStore Galley.Effects.CodeStore Galley.Effects.CustomBackendStore - Galley.Effects.FederatorAccess - Galley.Effects.ProposalStore Galley.Effects.Queue Galley.Effects.SearchVisibilityStore Galley.Effects.TeamFeatureStore @@ -165,7 +162,6 @@ library Galley.Env Galley.External.LegalHoldService Galley.External.LegalHoldService.Internal - Galley.Intra.Federator Galley.Intra.Util Galley.Keys Galley.Monad @@ -257,7 +253,7 @@ library Galley.Types.Clients Galley.Validation - ghc-options: + ghc-options: -fplugin=Polysemy.Plugin other-modules: Paths_galley hs-source-dirs: src build-depends: @@ -280,7 +276,6 @@ library , crypton , crypton-x509 , data-default - , data-timeout , errors >=2.0 , exceptions >=0.4 , extended @@ -305,6 +300,7 @@ library , pem , polysemy , polysemy-conc + , polysemy-plugin , polysemy-wire-zoo , prometheus-client , raw-strings-qq >=1.0 diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 1f45ab06963..e87d8eea908 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -78,10 +78,8 @@ import Galley.API.Util import Galley.Data.Scope (Scope (ReusableCode)) import Galley.Effects import Galley.Effects.CodeStore qualified as E -import Galley.Effects.FederatorAccess qualified as E -import Galley.Effects.ProposalStore qualified as E import Galley.Env (Env) -import Galley.Options +import Galley.Options (Opts) import Galley.Validation import Imports hiding ((\\)) import Polysemy @@ -105,6 +103,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Galley import Wire.API.Federation.API.Galley qualified as F +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.FederationStatus import Wire.API.MLS.Group.Serialisation qualified as Serialisation @@ -119,8 +118,11 @@ import Wire.API.User as User import Wire.BrigAPIAccess qualified as E import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..)) +import Wire.FederationAPIAccess qualified as E import Wire.FireAndForget qualified as E import Wire.NotificationSubsystem +import Wire.ProposalStore qualified as E import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation @@ -132,7 +134,8 @@ import Wire.UserList type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Constraint where HasConversationActionEffects 'ConversationJoinTag r = - ( Member BrigAPIAccess r, + ( -- TODO: Replace with subsystems + Member BrigAPIAccess r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS 'NotATeamMember) r, @@ -148,10 +151,9 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member LegalHoldStore r, Member ConversationStore r, @@ -166,10 +168,11 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con ( Member (Error InternalError) r, Member (Error NoChanges) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member ProposalStore r, Member ConversationStore r, Member Random r, @@ -180,9 +183,10 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member ConversationStore r, Member ProposalStore r, Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Error InternalError) r, Member Random r, @@ -199,7 +203,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'NotATeamMember) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ProposalStore r, Member TeamStore r ) @@ -218,10 +222,11 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (ErrorS 'InvalidTargetAccess) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member ProposalStore r, Member TeamStore r, Member TinyLog r, @@ -245,7 +250,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (Error NoChanges) r, Member BrigAPIAccess r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, @@ -267,7 +272,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (ErrorS InvalidOperation) r, Member ConversationStore r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ProposalStore r, Member Random r, @@ -363,21 +368,21 @@ type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: enforceFederationProtocol :: ( Member (Error FederationError) r, - Member (Input Opts) r + Member (Input ConversationSubsystemConfig) r ) => ProtocolTag -> [Remote ()] -> Sem r () enforceFederationProtocol proto domains = do unless (null domains) $ do - mAllowedProtos <- view (settings . federationProtocols) <$> input + mAllowedProtos <- federationProtocols <$> input unless (maybe True (elem proto) mAllowedProtos) $ throw FederationDisabledForProtocol checkFederationStatus :: ( Member (Error UnreachableBackends) r, Member (Error NonFederatingBackends) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => RemoteDomains -> Sem r () @@ -389,7 +394,7 @@ checkFederationStatus req = do getFederationStatus :: ( Member (Error UnreachableBackends) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => RemoteDomains -> Sem r FederationStatus @@ -504,7 +509,8 @@ performAction :: Member (Error FederationError) r, Member ConversationSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Sing tag -> Qualified UserId -> @@ -655,7 +661,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do then checkFederationStatus (RemoteDomains (invitedRemoteDomains <> existingRemoteDomains)) else -- even if there are no new remotes, we still need to check they are reachable void . (ensureNoUnreachableBackends =<<) $ - E.runFederatedConcurrentlyEither @_ @'Brig invitedRemoteUsers $ \_ -> + E.runFederatedConcurrentlyEither @_ @_ @'Brig invitedRemoteUsers $ \_ -> pure () conv :: StoredConversation @@ -860,7 +866,8 @@ updateLocalConversation :: SingI tag, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local ConvId -> Qualified UserId -> @@ -895,7 +902,8 @@ updateLocalConversationUnchecked :: HasConversationActionEffects tag r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local StoredConversation -> Qualified UserId -> @@ -1119,7 +1127,7 @@ notifyTypingIndicator :: ( Member Now r, Member (Input (Local ())) r, Member NotificationSubsystem r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => StoredConversation -> Qualified UserId -> diff --git a/services/galley/src/Galley/API/Action/Kick.hs b/services/galley/src/Galley/API/Action/Kick.hs index b9527adb718..4224dfe4a02 100644 --- a/services/galley/src/Galley/API/Action/Kick.hs +++ b/services/galley/src/Galley/API/Action/Kick.hs @@ -25,7 +25,6 @@ import Galley.API.Action.Leave import Galley.API.Action.Notify import Galley.API.Util import Galley.Effects -import Galley.Env (Env) import Imports hiding ((\\)) import Polysemy import Polysemy.Error @@ -36,6 +35,7 @@ import Wire.API.Conversation.Action import Wire.API.Event.LeaveReason import Wire.API.Federation.Error import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.StoredConversation @@ -53,7 +53,7 @@ kickMember :: Member NotificationSubsystem r, Member ProposalStore r, Member Now r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member ConversationStore r, Member TinyLog r, Member Random r diff --git a/services/galley/src/Galley/API/Action/Leave.hs b/services/galley/src/Galley/API/Action/Leave.hs index 4e4001c7499..8d717b9cccf 100644 --- a/services/galley/src/Galley/API/Action/Leave.hs +++ b/services/galley/src/Galley/API/Action/Leave.hs @@ -23,13 +23,13 @@ import Data.Qualified import Galley.API.MLS.Removal import Galley.API.Util import Galley.Effects -import Galley.Env (Env) import Imports hiding ((\\)) import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog import Wire.API.Federation.Error +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.StoredConversation @@ -44,7 +44,7 @@ leaveConversation :: Member NotificationSubsystem r, Member ProposalStore r, Member Random r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r ) => Qualified UserId -> diff --git a/services/galley/src/Galley/API/Action/Reset.hs b/services/galley/src/Galley/API/Action/Reset.hs index c1ca56064e1..296959f8ee1 100644 --- a/services/galley/src/Galley/API/Action/Reset.hs +++ b/services/galley/src/Galley/API/Action/Reset.hs @@ -26,8 +26,6 @@ import Galley.API.Action.Kick import Galley.API.MLS.Util import Galley.API.Util import Galley.Effects -import Galley.Effects.FederatorAccess -import Galley.Env import Imports import Polysemy import Polysemy.Error @@ -40,6 +38,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Federation.Version import Wire.API.MLS.Group.Serialisation as GroupId @@ -49,18 +48,19 @@ import Wire.API.Routes.Public.Galley.MLS import Wire.API.VersionInfo import Wire.ConversationStore import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.StoredConversation as Data resetLocalMLSMainConversation :: - ( Member (Input Env) r, - Member Now r, + ( Member Now r, Member (ErrorS MLSStaleMessage) r, Member (ErrorS ConvNotFound) r, Member (ErrorS InvalidOperation) r, Member BackendNotificationQueueAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ExternalAccess r, Member ConversationSubsystem r, Member NotificationSubsystem r, @@ -69,7 +69,9 @@ resetLocalMLSMainConversation :: Member Resource r, Member ConversationStore r, Member P.TinyLog r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + VersionedMonad Version (FederatorClient Brig), + Member (Input ConversationSubsystemConfig) r ) => Qualified UserId -> Local StoredConversation -> @@ -106,7 +108,7 @@ resetLocalMLSMainConversation qusr lcnv reset = do let remoteUsers = map (.id_) cnv.remoteMembers let targets = convBotsAndMembers cnv results <- - runFederatedConcurrentlyEither @_ @Brig remoteUsers $ + runFederatedConcurrentlyEither @_ @_ @Brig remoteUsers $ \_ -> do guardVersion $ \fedV -> fedV >= groupIdFedVersion GroupIdVersion2 let kick qvictim = do diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index 1fda288d627..60dae17eaaf 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -47,6 +47,7 @@ import Wire.API.Federation.Error import Wire.API.Routes.MultiTablePaging import Wire.BackendNotificationQueueAccess import Wire.ConversationStore (getConversation) +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) @@ -75,7 +76,8 @@ rmClient :: Member (Error InternalError) r, Member ProposalStore r, Member Random r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input ConversationSubsystemConfig) r ) => UserId -> ClientId -> diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 409f0249417..6de6c71e7c3 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -51,8 +51,7 @@ import Galley.API.Teams.Features.Get (getFeatureForTeam) import Galley.API.Util import Galley.App (Env) import Galley.Effects -import Galley.Effects.FederatorAccess qualified as E -import Galley.Options +import Galley.Options (Opts) import Galley.Types.Teams (notTeamMember) import Galley.Validation import Imports hiding ((\\)) @@ -67,6 +66,7 @@ import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.FederationStatus import Wire.API.Push.V2 qualified as PushV2 @@ -82,6 +82,8 @@ import Wire.API.Team.Permission hiding (self) import Wire.API.User import Wire.BrigAPIAccess import Wire.ConversationStore qualified as E +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess qualified as E import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now @@ -116,7 +118,7 @@ createGroupConversationUpToV3 :: Member (ErrorS ChannelsNotEnabled) r, Member (ErrorS NotAnMlsConversation) r, Member (Error UnreachableBackendsLegacy) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, @@ -127,7 +129,8 @@ createGroupConversationUpToV3 :: Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, Member Random r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> Maybe ConnId -> @@ -163,10 +166,11 @@ createGroupOwnConversation :: Member (ErrorS ChannelsNotEnabled) r, Member (ErrorS NotAnMlsConversation) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member LegalHoldStore r, Member TeamStore r, @@ -210,10 +214,11 @@ createGroupConversation :: Member (ErrorS ChannelsNotEnabled) r, Member (ErrorS NotAnMlsConversation) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member LegalHoldStore r, Member TeamStore r, @@ -259,7 +264,7 @@ createGroupConvAndMkResponse :: Member (Error InternalError) r, Member (Error InvalidInput) r, Member P.TinyLog r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member BrigAPIAccess r, Member ConversationStore r, @@ -269,7 +274,8 @@ createGroupConvAndMkResponse :: Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, Member Random r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> Maybe ConnId -> @@ -301,10 +307,11 @@ createGroupConversationGeneric :: Member (ErrorS ChannelsNotEnabled) r, Member (ErrorS NotAnMlsConversation) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member LegalHoldStore r, Member TeamStore r, @@ -354,7 +361,7 @@ createGroupConversationGeneric lusr conn newConv joinType = do ensureNoLegalholdConflicts :: ( Member (ErrorS 'MissingLegalholdConsent) r, - Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member LegalHoldStore r, Member TeamStore r, Member TeamSubsystem r @@ -500,7 +507,7 @@ createOne2OneConversation :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotConnected) r, Member (Error UnreachableBackendsLegacy) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member TeamStore r, @@ -587,7 +594,7 @@ createLegacyOne2OneConversationUnchecked :: Member (Error FederationError) r, Member (Error InternalError) r, Member (Error InvalidInput) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member P.TinyLog r @@ -631,7 +638,7 @@ createOne2OneConversationUnchecked :: Member (Error FederationError) r, Member (Error InternalError) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member P.TinyLog r @@ -656,7 +663,7 @@ createOne2OneConversationLocally :: Member (Error FederationError) r, Member (Error InternalError) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member P.TinyLog r @@ -711,7 +718,7 @@ createConnectConversation :: Member (Error InvalidInput) r, Member (ErrorS 'InvalidOperation) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member P.TinyLog r @@ -883,7 +890,7 @@ notifyCreatedConversation :: Member (Error FederationError) r, Member (Error InternalError) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member BackendNotificationQueueAccess r, Member Now r, diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index cb3b2d8b1e7..25920337ae0 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -77,6 +77,7 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Common (EmptyResponse (..)) import Wire.API.Federation.API.Galley hiding (id) +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Endpoint import Wire.API.Federation.Error import Wire.API.Federation.Version @@ -93,6 +94,7 @@ import Wire.API.ServantProto import Wire.API.User (BaseProtocolTag (..)) import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.FireAndForget qualified as E import Wire.NotificationSubsystem import Wire.Sem.Now (Now) @@ -144,7 +146,8 @@ onClientRemoved :: Member Now r, Member ProposalStore r, Member Random r, - Member TinyLog r + Member TinyLog r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> ClientRemovedRequest -> @@ -267,7 +270,7 @@ leaveConversation :: Member ConversationStore r, Member (Error InternalError) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, Member (Input Env) r, @@ -278,7 +281,8 @@ leaveConversation :: Member TinyLog r, Member TeamSubsystem r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> LeaveConversationRequest -> @@ -393,7 +397,7 @@ sendMessage :: Member ClientStore r, Member ConversationStore r, Member (Error InvalidInput) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, Member (Input (Local ())) r, @@ -424,10 +428,10 @@ onUserDeleted :: Member NotificationSubsystem r, Member (Input (Local ())) r, Member Now r, - Member (Input Env) r, Member ProposalStore r, Member Random r, - Member TinyLog r + Member TinyLog r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> UserDeletedConversationsNotification -> @@ -481,7 +485,7 @@ updateConversation :: Member (Error FederationError) r, Member (Error InvalidInput) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Error InternalError) r, Member ConversationSubsystem r, Member NotificationSubsystem r, @@ -499,7 +503,8 @@ updateConversation :: Member (Input (Local ())) r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamStore r + Member TeamStore r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> ConversationUpdateRequest -> @@ -621,7 +626,7 @@ sendMLSCommitBundle :: Member ExternalAccess r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, Member (Input (Local ())) r, @@ -637,7 +642,8 @@ sendMLSCommitBundle :: Member Random r, Member ProposalStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> MLSMessageSendRequest -> @@ -682,7 +688,7 @@ sendMLSMessage :: Member ExternalAccess r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input Env) r, @@ -739,7 +745,8 @@ leaveSubConversation :: Member (Input (Local ())) r, Member Resource r, Member TeamSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> LeaveSubConversationRequest -> @@ -971,7 +978,7 @@ queryGroupInfo origDomain req = updateTypingIndicator :: ( Member NotificationSubsystem r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationStore r, Member Now r, Member (Input (Local ())) r, diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index a60dd23a448..ce8c980a59e 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -92,6 +92,7 @@ import Wire.BackendNotificationQueueAccess import Wire.ConversationStore import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.LegalHoldStore as LegalHoldStore import Wire.NotificationSubsystem import Wire.Sem.Now (Now) @@ -341,7 +342,8 @@ rmUser :: Member TeamFeatureStore r, Member TeamStore r, Member (Input FanoutLimit) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> Maybe ConnId -> diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 70cddd73bfb..db530859c11 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -64,6 +64,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Provider.Service import Wire.API.Routes.Internal.Brig.Connection @@ -78,6 +79,7 @@ import Wire.API.User.Client.Prekey import Wire.BrigAPIAccess import Wire.ConversationStore import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.FireAndForget import Wire.LegalHoldStore qualified as LegalHoldData import Wire.NotificationSubsystem @@ -164,7 +166,7 @@ removeSettingsInternalPaging :: Member (ErrorS OperationDenied) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, @@ -182,7 +184,8 @@ removeSettingsInternalPaging :: Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, Member (Input (FeatureDefaults LegalholdConfig)) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> TeamId -> @@ -212,7 +215,7 @@ removeSettings :: Member (ErrorS OperationDenied) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, @@ -227,7 +230,8 @@ removeSettings :: Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, Member (Input (FeatureDefaults LegalholdConfig)) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => UserId -> TeamId -> @@ -269,7 +273,7 @@ removeSettings' :: Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, @@ -285,7 +289,8 @@ removeSettings' :: Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => TeamId -> Sem r () @@ -323,7 +328,7 @@ grantConsent :: Member (ErrorS 'TeamMemberNotFound) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -335,7 +340,8 @@ grantConsent :: Member TeamStore r, Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> TeamId -> @@ -372,7 +378,7 @@ requestDevice :: Member (ErrorS 'UserLegalHoldAlreadyEnabled) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, @@ -388,7 +394,8 @@ requestDevice :: Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, Member (Input (FeatureDefaults LegalholdConfig)) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> TeamId -> @@ -468,7 +475,7 @@ approveDevice :: Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member (ErrorS 'UserLegalHoldNotPending) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, @@ -484,7 +491,8 @@ approveDevice :: Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, Member (Input (FeatureDefaults LegalholdConfig)) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -548,7 +556,7 @@ disableForUser :: Member (ErrorS OperationDenied) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -562,7 +570,8 @@ disableForUser :: Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> TeamId -> @@ -615,7 +624,7 @@ changeLegalholdStatusAndHandlePolicyConflicts :: Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -627,7 +636,8 @@ changeLegalholdStatusAndHandlePolicyConflicts :: Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => TeamId -> Local UserId -> @@ -734,7 +744,7 @@ handleGroupConvPolicyConflicts :: Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -745,7 +755,8 @@ handleGroupConvPolicyConflicts :: Member TeamStore r, Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> UserLegalHoldStatus -> diff --git a/services/galley/src/Galley/API/MLS/CheckClients.hs b/services/galley/src/Galley/API/MLS/CheckClients.hs index 6a788060d76..9822664de18 100644 --- a/services/galley/src/Galley/API/MLS/CheckClients.hs +++ b/services/galley/src/Galley/API/MLS/CheckClients.hs @@ -36,6 +36,7 @@ import Polysemy import Polysemy.Error import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage @@ -45,7 +46,7 @@ import Wire.ConversationStore.MLS.Types checkClients :: ( Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS MLSClientMismatch) r, Member (ErrorS MLSIdentityMismatch) r, Member (Error MLSProtocolError) r @@ -132,7 +133,7 @@ mkClientData clientInfo = -- | Get list of mls clients from Brig (local or remote). getClientData :: ( Member BrigAPIAccess r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local x -> CipherSuiteTag -> diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index ddfd05776bb..966693c5940 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -36,7 +36,6 @@ import Galley.API.MLS.Conversation import Galley.API.MLS.IncomingMessage import Galley.API.MLS.Proposal import Galley.Effects -import Galley.Effects.FederatorAccess import Galley.Env import Galley.Options import Imports @@ -52,6 +51,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Brig +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Endpoint import Wire.API.Federation.Error import Wire.API.Federation.Version @@ -67,6 +67,8 @@ import Wire.API.User.Client import Wire.BrigAPIAccess import Wire.ConversationStore import Wire.ConversationStore.MLS.Types +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.TeamCollaboratorsSubsystem @@ -87,7 +89,8 @@ type HasProposalActionEffects r = Member (ErrorS 'MLSSelfRemovalNotAllowed) r, Member (ErrorS 'GroupIdVersionNotSupported) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, + Member (Input ConversationSubsystemConfig) r, Member (Input Env) r, Member (Input Opts) r, Member Now r, @@ -147,7 +150,7 @@ incrementEpoch (SubConv c s) = do getClientInfo :: ( Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Error FederationError) r ) => Local x -> @@ -157,7 +160,7 @@ getClientInfo :: getClientInfo loc = foldQualified loc getLocalMLSClients getRemoteMLSClients getRemoteMLSClients :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, Member (Error FederationError) r ) => Remote UserId -> @@ -175,7 +178,7 @@ getRemoteMLSClients rusr suite = do getSingleClientInfo :: ( Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Error FederationError) r ) => Local x -> @@ -186,7 +189,7 @@ getSingleClientInfo :: getSingleClientInfo loc = foldQualified loc getLocalMLSClient getRemoteMLSClient getRemoteMLSClient :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, Member (Error FederationError) r ) => Remote UserId -> @@ -245,7 +248,7 @@ checkUpdatePath :: Member (Error MLSProtocolError) r, Member (Error FederationError) r, Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS MLSInvalidLeafNodeSignature) r ) => Local ConvOrSubConv -> diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index 454bca113b5..4fe175b8507 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -40,10 +40,10 @@ import Galley.API.MLS.Proposal import Galley.API.MLS.Util import Galley.API.Util import Galley.Effects -import Galley.Effects.ProposalStore import Imports import Polysemy import Polysemy.Error +import Polysemy.Input (Input) import Polysemy.Resource (Resource) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action @@ -61,6 +61,8 @@ import Wire.API.Unreachable import Wire.ConversationStore import Wire.ConversationStore.MLS.Types import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.ProposalStore import Wire.StoredConversation import Wire.TeamSubsystem (TeamSubsystem) @@ -79,7 +81,8 @@ processInternalCommit :: Member Random r, Member (ErrorS MLSInvalidLeafNodeSignature) r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => SenderIdentity -> Maybe ConnId -> @@ -258,7 +261,11 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdat pure events addMembers :: - (HasProposalActionEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r, Member TeamSubsystem r) => + ( HasProposalActionEffects r, + Member ConversationSubsystem r, + Member MLSCommitLockStore r, + Member TeamSubsystem r + ) => Qualified UserId -> Maybe ConnId -> Local ConvOrSubConv -> @@ -282,7 +289,11 @@ addMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of SubConv _ _ -> pure [] removeMembers :: - (HasProposalActionEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r, Member TeamSubsystem r) => + ( HasProposalActionEffects r, + Member ConversationSubsystem r, + Member MLSCommitLockStore r, + Member TeamSubsystem r + ) => Qualified UserId -> Maybe ConnId -> Local ConvOrSubConv -> diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs index 1ca5c0d729a..241dad0b2e6 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfo.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -24,7 +24,6 @@ import Galley.API.MLS.Enabled import Galley.API.MLS.Util import Galley.API.Util import Galley.Effects -import Galley.Effects.FederatorAccess qualified as E import Galley.Env import Imports import Polysemy @@ -34,10 +33,12 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation import Wire.ConversationStore qualified as E +import Wire.FederationAPIAccess qualified as E type MLSGroupInfoStaticErrors = '[ ErrorS 'ConvNotFound, @@ -48,7 +49,7 @@ type MLSGroupInfoStaticErrors = getGroupInfo :: ( Member ConversationStore r, Member (Error FederationError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Input Env) r ) => (Members MLSGroupInfoStaticErrors r) => @@ -77,7 +78,7 @@ getGroupInfoFromLocalConv qusr lcnvId = do getGroupInfoFromRemoteConv :: ( Member (Error FederationError) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => (Members MLSGroupInfoStaticErrors r) => Local UserId -> diff --git a/services/galley/src/Galley/API/MLS/Keys.hs b/services/galley/src/Galley/API/MLS/Keys.hs index 71895166859..89c610b71e4 100644 --- a/services/galley/src/Galley/API/MLS/Keys.hs +++ b/services/galley/src/Galley/API/MLS/Keys.hs @@ -18,25 +18,24 @@ module Galley.API.MLS.Keys (getMLSRemovalKey, SomeKeyPair (..)) where import Control.Error.Util (hush) -import Control.Lens (view) import Data.Proxy -import Galley.Env import Imports hiding (getFirst) import Polysemy import Polysemy.Error import Polysemy.Input import Wire.API.MLS.CipherSuite import Wire.API.MLS.Keys +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..)) data SomeKeyPair where SomeKeyPair :: forall ss. (IsSignatureScheme ss) => Proxy ss -> KeyPair ss -> SomeKeyPair getMLSRemovalKey :: - (Member (Input Env) r) => + (Member (Input ConversationSubsystemConfig) r) => SignatureSchemeTag -> Sem r (Maybe SomeKeyPair) getMLSRemovalKey ss = fmap hush . runError @() $ do - keysByPurpose <- note () =<< inputs (view mlsKeys) + keysByPurpose <- note () =<< inputs (.mlsKeys) let keys = keysByPurpose.removal case ss of Ed25519 -> pure $ SomeKeyPair (Proxy @Ed25519) (mlsKeyPair_ed25519 keys) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index ae77df7a522..47103a0d90f 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -58,7 +58,6 @@ import Galley.API.MLS.Util import Galley.API.MLS.Welcome (sendWelcomes) import Galley.API.Util import Galley.Effects -import Galley.Effects.FederatorAccess import Imports import Polysemy import Polysemy.Error @@ -73,6 +72,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit hiding (output) @@ -88,6 +88,8 @@ import Wire.API.Team.LegalHold import Wire.ConversationStore import Wire.ConversationStore.MLS.Types import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now qualified as Now import Wire.StoredConversation @@ -183,7 +185,8 @@ postMLSCommitBundle :: HasProposalEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local x -> Qualified UserId -> @@ -213,7 +216,8 @@ postMLSCommitBundleFromLocalUser :: HasProposalEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Version -> Local UserId -> @@ -247,7 +251,8 @@ postMLSCommitBundleToLocalConv :: HasProposalEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Qualified UserId -> ClientId -> @@ -384,7 +389,7 @@ postMLSCommitBundleToRemoteConv :: Member (Error MLSOutOfSyncError) r, Member (Input EnableOutOfSyncCheck) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationStore r, Member TinyLog r diff --git a/services/galley/src/Galley/API/MLS/Migration.hs b/services/galley/src/Galley/API/MLS/Migration.hs index bb3da3678b4..64bc3741ae6 100644 --- a/services/galley/src/Galley/API/MLS/Migration.hs +++ b/services/galley/src/Galley/API/MLS/Migration.hs @@ -21,14 +21,17 @@ import Brig.Types.Intra import Data.Qualified import Data.Set qualified as Set import Data.Time -import Galley.Effects.FederatorAccess import Imports import Polysemy +import Polysemy.Error (Error) import Wire.API.Federation.API +import Wire.API.Federation.Client (FederatorClient) +import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.API.User import Wire.BrigAPIAccess import Wire.ConversationStore.MLS.Types +import Wire.FederationAPIAccess import Wire.StoredConversation -- | Similar to @Ap f All@, but short-circuiting. @@ -48,7 +51,8 @@ instance (Monad f) => Monoid (ApAll f) where checkMigrationCriteria :: ( Member BrigAPIAccess r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r ) => UTCTime -> MLSConversation -> diff --git a/services/galley/src/Galley/API/MLS/OutOfSync.hs b/services/galley/src/Galley/API/MLS/OutOfSync.hs index e1d133f1693..955efdd84ff 100644 --- a/services/galley/src/Galley/API/MLS/OutOfSync.hs +++ b/services/galley/src/Galley/API/MLS/OutOfSync.hs @@ -32,6 +32,7 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.OutOfSync @@ -43,7 +44,7 @@ import Wire.StoredConversation checkConversationOutOfSync :: ( Member ConversationStore r, Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Error MLSOutOfSyncError) r, Member (Input EnableOutOfSyncCheck) r ) => @@ -66,7 +67,7 @@ checkConversationOutOfSync newMembers lConvOrSub ciphersuite = case tUnqualified checkOutOfSyncUser :: ( Member BrigAPIAccess r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local x -> CipherSuiteTag -> diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index edd9beb581e..72d2db09d3e 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -42,7 +42,6 @@ import Galley.API.Error import Galley.API.MLS.IncomingMessage import Galley.API.Util import Galley.Effects -import Galley.Effects.ProposalStore import Galley.Env import Galley.Options import Imports @@ -55,6 +54,7 @@ import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.AuthenticatedContent import Wire.API.MLS.CipherSuite @@ -70,6 +70,7 @@ import Wire.API.Message import Wire.BrigAPIAccess import Wire.ConversationStore.MLS.Types import Wire.NotificationSubsystem +import Wire.ProposalStore import Wire.Sem.Now (Now) import Wire.TeamCollaboratorsSubsystem @@ -129,7 +130,7 @@ type HasProposalEffects r = Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Input Env) r, Member (Input (Local ())) r, Member (Input Opts) r, @@ -137,7 +138,6 @@ type HasProposalEffects r = Member LegalHoldStore r, Member ProposalStore r, Member TeamStore r, - Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r ) diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index de90336757d..a65e4a0c80b 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -35,8 +35,6 @@ import Galley.API.MLS.Conversation import Galley.API.MLS.Keys import Galley.API.MLS.Propagate import Galley.Effects -import Galley.Effects.ProposalStore -import Galley.Env import Imports import Polysemy import Polysemy.Error @@ -55,7 +53,9 @@ import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.ConversationStore import Wire.ConversationStore.MLS.Types +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem +import Wire.ProposalStore import Wire.Sem.Now (Now) import Wire.Sem.Random import Wire.StoredConversation @@ -70,7 +70,7 @@ createAndSendRemoveProposals :: Member ExternalAccess r, Member NotificationSubsystem r, Member ProposalStore r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Random r, Traversable t ) => @@ -128,7 +128,7 @@ removeClientsWithClientMapRecursively :: Member NotificationSubsystem r, Member ConversationStore r, Member ProposalStore r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Random r, Traversable f ) => @@ -159,7 +159,7 @@ removeClientsFromSubConvs :: Member ExternalAccess r, Member NotificationSubsystem r, Member ProposalStore r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Random r, Traversable f, Member ConversationStore r @@ -195,7 +195,7 @@ removeClient :: Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member ConversationStore r, Member ProposalStore r, @@ -231,7 +231,7 @@ removeUser :: Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member ConversationStore r, Member ProposalStore r, @@ -277,7 +277,7 @@ removeExtraneousClients :: Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member ConversationStore r, Member ProposalStore r, diff --git a/services/galley/src/Galley/API/MLS/Reset.hs b/services/galley/src/Galley/API/MLS/Reset.hs index c47dc8f8cda..5d9515c9722 100644 --- a/services/galley/src/Galley/API/MLS/Reset.hs +++ b/services/galley/src/Galley/API/MLS/Reset.hs @@ -35,11 +35,13 @@ import Polysemy.TinyLog qualified as P import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.SubConversation import Wire.API.Routes.Public.Galley.MLS import Wire.ConversationStore import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.TeamCollaboratorsSubsystem @@ -60,7 +62,7 @@ resetMLSConversation :: Member (ErrorS GroupIdVersionNotSupported) r, Member BackendNotificationQueueAccess r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ExternalAccess r, Member (Error FederationError) r, Member BrigAPIAccess r, @@ -72,7 +74,8 @@ resetMLSConversation :: Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> MLSReset -> @@ -113,7 +116,7 @@ resetRemoteMLSConversation :: Member (Error InternalError) r, Member BrigAPIAccess r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationStore r ) => diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 3badd394185..3616c18e773 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -44,7 +44,6 @@ import Galley.API.MLS.Util import Galley.API.Util import Galley.App (Env) import Galley.Effects -import Galley.Effects.FederatorAccess import Imports import Polysemy import Polysemy.Error @@ -57,6 +56,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.Credential import Wire.API.MLS.Group.Serialisation @@ -66,6 +66,8 @@ import Wire.API.MLS.SubConversation import Wire.API.Routes.Public.Galley.MLS import Wire.ConversationStore qualified as Eff import Wire.ConversationStore.MLS.Types as Eff +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.StoredConversation @@ -84,7 +86,7 @@ getSubConversation :: Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'MLSSubConvUnsupportedConvType) r, Member (Error FederationError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TeamSubsystem r ) => Local UserId -> @@ -136,7 +138,8 @@ getRemoteSubConversation :: '[ ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied, ErrorS 'MLSSubConvUnsupportedConvType, - FederatorAccess + Error FederationError, + FederationAPIAccess FederatorClient ] r, RethrowErrors MLSGetSubConvStaticErrors r @@ -163,7 +166,7 @@ getSubConversationGroupInfo :: ( Members '[ ConversationStore, Error FederationError, - FederatorAccess, + FederationAPIAccess FederatorClient, Input Env ] r, @@ -207,7 +210,7 @@ deleteSubConversation :: Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'MLSStaleMessage) r, Member (Error FederationError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Input Env) r, Member Resource r, Member Eff.MLSCommitLockStore r, @@ -232,7 +235,7 @@ resetRemoteSubConversation :: Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'MLSStaleMessage) r, Member (Error FederationError) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local UserId -> Remote ConvId -> @@ -260,7 +263,7 @@ type HasLeaveSubConversationEffects r = ( Member BackendNotificationQueueAccess r, Member ConversationStore r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member Now r, @@ -285,7 +288,8 @@ leaveSubConversation :: Member Resource r, Members LeaveSubConversationStaticErrors r, Member Eff.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ClientId -> @@ -311,7 +315,8 @@ leaveLocalSubConversation :: Member Resource r, Members LeaveSubConversationStaticErrors r, Member Eff.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => ClientIdentity -> Local ConvId -> @@ -352,7 +357,7 @@ leaveRemoteSubConversation :: ErrorS 'ConvAccessDenied, Error FederationError, Error MLSProtocolError, - FederatorAccess + FederationAPIAccess FederatorClient ] r ) => diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 97b6151abca..1127873fc5d 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -26,7 +26,6 @@ import Data.Set qualified as Set import Data.Text qualified as T import Galley.Data.Types import Galley.Effects -import Galley.Effects.ProposalStore import Imports import Polysemy import Polysemy.Error @@ -44,6 +43,7 @@ import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.ConversationStore +import Wire.ProposalStore getLocalConvForUser :: ( Member (ErrorS 'ConvNotFound) r, diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index e36aadac178..115ad8faab1 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -30,7 +30,6 @@ import Data.Map qualified as Map import Data.Qualified import Data.Time import Galley.API.Push -import Galley.Effects.FederatorAccess import Imports import Network.Wai.Utilities.JSONResponse import Polysemy @@ -41,6 +40,7 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.Credential import Wire.API.MLS.Message @@ -50,12 +50,13 @@ import Wire.API.MLS.Welcome import Wire.API.Message import Wire.API.Push.V2 (RecipientClients (..)) import Wire.ExternalAccess +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now sendWelcomes :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, Member ExternalAccess r, Member P.TinyLog r, Member Now r, @@ -104,7 +105,7 @@ sendLocalWelcomes qcnv qusr con now welcome lclients = do newMessagePush mempty con defMessageMetadata rcpts e sendRemoteWelcomes :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Qualified ConvId -> diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index fcf2bf5e511..6ca08e514d0 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -54,7 +54,6 @@ import Galley.API.Push import Galley.API.Util import Galley.Effects import Galley.Effects.ClientStore -import Galley.Effects.FederatorAccess import Galley.Env import Galley.Options import Galley.Types.Clients qualified as Clients @@ -83,6 +82,7 @@ import Wire.API.UserMap (UserMap (..)) import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess import Wire.ConversationStore +import Wire.FederationAPIAccess import Wire.NotificationSubsystem (NotificationSubsystem) import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now @@ -221,7 +221,8 @@ checkMessageClients sender participantMap recipientMap mismatchStrat = ) getRemoteClients :: - (Member FederatorAccess r) => + forall r. + (Member (FederationAPIAccess FederatorClient) r) => [RemoteMember] -> Sem r [Either (Remote [UserId], FederationError) (Map (Domain, UserId) (Set ClientId))] getRemoteClients remoteMembers = @@ -236,7 +237,9 @@ getRemoteClients remoteMembers = <$> fedClient @'Brig @"get-user-clients" (GetUserClients uids) postRemoteOtrMessage :: - (Member FederatorAccess r) => + ( Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r + ) => Local UserId -> Remote ConvId -> ByteString -> @@ -367,7 +370,7 @@ postQualifiedOtrMessage :: ( Member BrigAPIAccess r, Member ClientStore r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member ExternalAccess r, Member (Input Opts) r, diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 2bbd130146a..4dba67cd2c3 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -78,7 +78,6 @@ import Galley.API.Util import Galley.Data.Types (Code (codeConversation)) import Galley.Data.Types qualified as Data import Galley.Effects -import Galley.Effects.FederatorAccess qualified as E import Galley.Env import Galley.Options import Imports @@ -111,6 +110,7 @@ import Wire.API.Team.Member (HiddenPerm (..), TeamMember) import Wire.API.User import Wire.ConversationStore qualified as E import Wire.ConversationStore.MLS.Types +import Wire.FederationAPIAccess qualified as E import Wire.HashPassword (HashPassword) import Wire.RateLimit import Wire.Sem.Paging.Cassandra @@ -181,7 +181,7 @@ getConversation :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r, Member TeamSubsystem r ) => @@ -202,7 +202,7 @@ getOwnConversation :: Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r, Member TeamSubsystem r ) => @@ -221,7 +221,7 @@ getRemoteConversation :: Member (ErrorS ConvNotFound) r, Member (Error FederationError) r, Member TinyLog r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local UserId -> Remote ConvId -> @@ -237,7 +237,7 @@ getRemoteConversations :: ( Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'ConvNotFound) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Local UserId -> @@ -306,7 +306,7 @@ partitionGetConversationFailures = bimap concat concat . partitionEithers . map getRemoteConversationsWithFailures :: ( Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Local UserId -> @@ -502,7 +502,7 @@ getConversationsInternal luser mids mstart msize = do listConversations :: ( Member ConversationStore r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Local UserId -> @@ -586,7 +586,7 @@ getSelfMember :: Member (ErrorS ConvNotFound) r, Member (Error FederationError) r, Member TinyLog r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local UserId -> Qualified ConvId -> @@ -778,7 +778,7 @@ getMLSOne2OneConversationV5 :: Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, Member (ErrorS 'MLSFederatedOne2OneNotSupported) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, @@ -800,7 +800,7 @@ getMLSOne2OneConversationInternal :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, @@ -820,7 +820,7 @@ getMLSOne2OneConversationV6 :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, @@ -847,7 +847,7 @@ getMLSOne2OneConversation :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, @@ -888,7 +888,7 @@ getRemoteMLSOne2OneConversation :: ( Member (Error InternalError) r, Member (Error FederationError) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS MLSNotEnabled) r, Member TinyLog r ) => @@ -946,7 +946,7 @@ isMLSOne2OneEstablished :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TinyLog r ) => Local UserId -> @@ -977,7 +977,7 @@ isRemoteMLSOne2OneEstablished :: ( Member (ErrorS 'NotConnected) r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS MLSNotEnabled) r, Member TinyLog r ) => diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 0324fe1a16e..041011c9d60 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -105,6 +105,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.LeaveReason import Wire.API.Event.Team +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Push.V2 (RecipientClients (RecipientClientsAll)) import Wire.API.Routes.Internal.Galley.TeamsIntra @@ -129,6 +130,7 @@ import Wire.BrigAPIAccess qualified as Brig import Wire.BrigAPIAccess qualified as E import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.ListItems qualified as E import Wire.NotificationSubsystem import Wire.Sem.Now @@ -971,13 +973,14 @@ deleteTeamConversation :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS ('ActionDenied 'Public.DeleteConversation)) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ProposalStore r, Member ConversationSubsystem r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index a8b28c6c3c0..0359adec7d2 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -62,12 +62,14 @@ import Wire.API.Conversation.Role (Action (RemoveConversationMember)) import Wire.API.Error (ErrorS) import Wire.API.Error.Galley import Wire.API.Event.FeatureConfig +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.BrigAPIAccess (updateSearchVisibilityInbound) import Wire.ConversationStore (MLSCommitLockStore) import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Paging @@ -334,7 +336,7 @@ instance SetFeatureConfig LegalholdConfig where Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, @@ -353,7 +355,8 @@ instance SetFeatureConfig LegalholdConfig where Member TeamCollaboratorsSubsystem r, Member MLSCommitLockStore r, Member (Input FanoutLimit) r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) prepareFeature tid feat = do diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index c621043dba2..ee4220a500b 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -102,7 +102,6 @@ import Galley.Data.Types import Galley.Effects import Galley.Effects.ClientStore qualified as E import Galley.Effects.CodeStore qualified as E -import Galley.Effects.FederatorAccess qualified as E import Galley.Env import Galley.Options import Imports hiding (forkIO) @@ -124,6 +123,7 @@ import Wire.API.Event.Conversation import Wire.API.Event.LeaveReason import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Message import Wire.API.Routes.Public (ZHostValue) @@ -136,7 +136,9 @@ import Wire.API.User.Client import Wire.API.UserGroup import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.ExternalAccess qualified as E +import Wire.FederationAPIAccess qualified as E import Wire.HashPassword as HashPassword import Wire.NotificationSubsystem import Wire.RateLimit @@ -274,11 +276,12 @@ type UpdateConversationAccessEffects = ErrorS 'InvalidOperation, ErrorS 'InvalidTargetAccess, ExternalAccess, - FederatorAccess, + FederationAPIAccess FederatorClient, FireAndForget, NotificationSubsystem, ConversationSubsystem, Input Env, + Input ConversationSubsystemConfig, ProposalStore, Random, TeamStore, @@ -333,14 +336,15 @@ updateConversationReceiptMode :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'MLSReadReceiptsNotAllowed) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -368,7 +372,7 @@ updateRemoteConversation :: forall tag r. ( Member BrigAPIAccess r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member ConversationStore r, @@ -414,14 +418,15 @@ updateConversationReceiptModeUnqualified :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'MLSReadReceiptsNotAllowed) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -440,7 +445,8 @@ updateConversationMessageTimer :: Member ConversationSubsystem r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -473,7 +479,8 @@ updateConversationMessageTimerUnqualified :: Member ConversationSubsystem r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -492,13 +499,14 @@ deleteLocalConversation :: Member (ErrorS ('ActionDenied 'DeleteConversation)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member ProposalStore r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -736,13 +744,14 @@ updateConversationProtocolWithLocalUser :: Member NotificationSubsystem r, Member ConversationSubsystem r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member Random r, Member ProposalStore r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -784,11 +793,12 @@ updateChannelAddPermission :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS 'InvalidTargetAccess) r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -829,7 +839,8 @@ joinConversationByReusableCode :: Member TeamFeatureStore r, Member HashPassword r, Member RateLimit r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -851,7 +862,7 @@ joinConversationById :: Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TooManyMembers) r, Member ConversationSubsystem r, - Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member TeamSubsystem r ) => Local UserId -> @@ -870,7 +881,7 @@ joinConversation :: Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TooManyMembers) r, Member ConversationSubsystem r, - Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member ConversationStore r, Member TeamSubsystem r ) => @@ -929,11 +940,9 @@ addMembers :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, @@ -942,7 +951,8 @@ addMembers :: Member TinyLog r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -985,11 +995,9 @@ addMembersUnqualifiedV2 :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, @@ -998,7 +1006,8 @@ addMembersUnqualifiedV2 :: Member TinyLog r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1030,11 +1039,9 @@ addMembersUnqualified :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, @@ -1043,7 +1050,8 @@ addMembersUnqualified :: Member TinyLog r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1078,10 +1086,9 @@ replaceMembers :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, @@ -1092,7 +1099,8 @@ replaceMembers :: Member E.MLSCommitLockStore r, Member UserGroupStore r, Member ConversationSubsystem r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1221,7 +1229,8 @@ updateOtherMemberLocalConv :: Member ConversationSubsystem r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local ConvId -> Local UserId -> @@ -1247,7 +1256,8 @@ updateOtherMemberUnqualified :: Member ConversationSubsystem r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1272,7 +1282,8 @@ updateOtherMember :: Member ConversationSubsystem r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1303,7 +1314,7 @@ removeMemberUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -1313,7 +1324,8 @@ removeMemberUnqualified :: Member TinyLog r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1334,7 +1346,7 @@ removeMemberQualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -1344,7 +1356,8 @@ removeMemberQualified :: Member TinyLog r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1366,7 +1379,8 @@ pattern EdMembersLeaveRemoved :: QualifiedUserIdList -> EventData pattern EdMembersLeaveRemoved l = EdMembersLeave EdReasonRemoved l removeMemberFromRemoteConv :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member (ErrorS 'ConvNotFound) r, Member Now r @@ -1412,7 +1426,7 @@ removeMemberFromLocalConv :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -1422,7 +1436,8 @@ removeMemberFromLocalConv :: Member TinyLog r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local ConvId -> Local UserId -> @@ -1461,7 +1476,7 @@ removeMemberFromChannel :: Member ProposalStore r, Member Now r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Error InternalError) r, @@ -1470,7 +1485,8 @@ removeMemberFromChannel :: Member (Error FederationError) r, Member BackendNotificationQueueAccess r, Member ConversationStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Qualified UserId -> Local StoredConversation -> @@ -1494,7 +1510,8 @@ postProteusMessage :: ( Member BrigAPIAccess r, Member ClientStore r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r, Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, Member ExternalAccess r, @@ -1572,7 +1589,7 @@ postBotMessageUnqualified :: Member ClientStore r, Member ConversationStore r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, Member (Input (Local ())) r, @@ -1627,7 +1644,7 @@ postOtrMessageUnqualified :: ( Member BrigAPIAccess r, Member ClientStore r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member ExternalAccess r, Member NotificationSubsystem r, @@ -1661,7 +1678,8 @@ updateConversationName :: Member TeamStore r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1688,7 +1706,8 @@ updateUnqualifiedConversationName :: Member TeamStore r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1711,7 +1730,8 @@ updateLocalConversationName :: Member TeamStore r, Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, - Member TeamSubsystem r + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1728,7 +1748,8 @@ memberTyping :: Member (Input (Local ())) r, Member Now r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r, Member TeamSubsystem r ) => Local UserId -> @@ -1766,7 +1787,8 @@ memberTypingUnqualified :: Member (Input (Local ())) r, Member Now r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r, Member TeamSubsystem r ) => Local UserId -> @@ -1788,7 +1810,7 @@ addBot :: Member (ErrorS 'TooManyMembers) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r ) => Local UserId -> diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 3107aa96d19..a89dfde31a4 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -20,7 +20,7 @@ module Galley.API.Util where -import Control.Lens (to, view, (^.)) +import Control.Lens (view, (^.)) import Control.Monad.Extra (allM, anyM) import Control.Monad.Trans.Maybe import Data.Bifunctor @@ -46,9 +46,7 @@ import Galley.Data.Types qualified as DataTypes import Galley.Effects import Galley.Effects.ClientStore import Galley.Effects.CodeStore -import Galley.Effects.FederatorAccess import Galley.Env -import Galley.Options import Galley.Types.Clients (Clients, fromUserClients) import Galley.Types.Conversations.Roles import Galley.Types.Teams @@ -70,6 +68,7 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Federation.Version import Wire.API.MLS.Group.Serialisation @@ -78,7 +77,6 @@ import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util import Wire.API.Team.Collaborator import Wire.API.Team.Collaborator qualified as CollaboratorPermission (CollaboratorPermission (..)) -import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Member qualified as Mem import Wire.API.Team.Member.Error @@ -89,7 +87,9 @@ import Wire.API.VersionInfo import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess import Wire.ConversationStore +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..)) import Wire.ExternalAccess +import Wire.FederationAPIAccess import Wire.HashPassword (HashPassword) import Wire.HashPassword qualified as HashPassword import Wire.LegalHoldStore @@ -303,7 +303,8 @@ ensureConvRoleNotElevated origMember targetRole = do checkGroupIdSupport :: ( Member (ErrorS GroupIdVersionNotSupported) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r, + VersionedMonad Version (FederatorClient Brig) ) => Local x -> StoredConversation -> @@ -323,7 +324,7 @@ checkGroupIdSupport loc conv joinAction = void $ runMaybeT $ do -- check that each remote backend is compatible with group ID version >= 2 let (_, remoteUsers) = partitionQualified loc joinAction.users lift - . (failOnFirstError <=< runFederatedConcurrentlyEither @_ @Brig remoteUsers) + . (failOnFirstError <=< runFederatedConcurrentlyEither @_ @_ @Brig remoteUsers) $ \_ -> do guardVersion $ \fedV -> fedV >= groupIdFedVersion GroupIdVersion2 where @@ -918,7 +919,7 @@ registerRemoteConversationMemberships :: Member (Error UnreachableBackends) r, Member (Error FederationError) r, Member BackendNotificationQueueAccess r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => -- | The time stamp when the conversation was created UTCTime -> @@ -1051,15 +1052,15 @@ getLHStatus teamOfUser other = do pure $ maybe defUserLegalHoldStatus (view legalHoldStatus) mMember anyLegalholdActivated :: - ( Member (Input Opts) r, + ( Member (Input ConversationSubsystemConfig) r, Member TeamStore r, Member TeamSubsystem r ) => [UserId] -> Sem r Bool anyLegalholdActivated uids = do - opts <- input - case view (settings . featureFlags . to npProject) opts of + cfg <- input + case legalholdDefaults cfg of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> check FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> check @@ -1070,7 +1071,7 @@ anyLegalholdActivated uids = do anyM (\uid -> userLHEnabled <$> getLHStatus (Map.lookup uid teamsOfUsers) uid) uidsPage allLegalholdConsentGiven :: - ( Member (Input Opts) r, + ( Member (Input ConversationSubsystemConfig) r, Member LegalHoldStore r, Member TeamStore r, Member TeamSubsystem r @@ -1078,8 +1079,8 @@ allLegalholdConsentGiven :: [UserId] -> Sem r Bool allLegalholdConsentGiven uids = do - opts <- input - case view (settings . featureFlags . to npProject) opts of + cfg <- input + case legalholdDefaults cfg of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> do flip allM (chunksOf 32 uids) $ \uidsPage -> do @@ -1124,7 +1125,7 @@ getTeamMembersForFanout tid = do ensureMemberLimit :: ( Foldable f, ( Member (ErrorS 'TooManyMembers) r, - Member (Input Opts) r + Member (Input ConversationSubsystemConfig) r ) ) => ProtocolTag -> @@ -1133,8 +1134,8 @@ ensureMemberLimit :: Sem r () ensureMemberLimit ProtocolMLSTag _ _ = pure () ensureMemberLimit _ old new = do - o <- input - let maxSize = fromIntegral (o ^. settings . maxConvSize) + cfg <- input + let maxSize = fromIntegral (cfg.maxConvSize) when (length old + length new > maxSize) $ throwS @'TooManyMembers diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index aef93717a16..7808ea80ed3 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -56,7 +56,6 @@ import Galley.API.Error import Galley.Cassandra.Client import Galley.Cassandra.Code import Galley.Cassandra.CustomBackend -import Galley.Cassandra.Proposal import Galley.Cassandra.SearchVisibility import Galley.Cassandra.Team ( interpretInternalTeamListToCassandra, @@ -69,7 +68,6 @@ import Galley.Cassandra.TeamNotifications import Galley.Effects import Galley.Env import Galley.External.LegalHoldService.Internal qualified as LHInternal -import Galley.Intra.Federator import Galley.Keys import Galley.Monad (runApp) import Galley.Options hiding (brig, endpoint, federator) @@ -112,9 +110,10 @@ import Wire.BackendNotificationQueueAccess.RabbitMq qualified as BackendNotifica import Wire.BrigAPIAccess.Rpc import Wire.ConversationStore.Cassandra import Wire.ConversationStore.Postgres -import Wire.ConversationSubsystem.Interpreter (interpretConversationSubsystem) +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..), interpretConversationSubsystem) import Wire.Error import Wire.ExternalAccess.External +import Wire.FederationAPIAccess.Interpreter import Wire.FireAndForget import Wire.GundeckAPIAccess (runGundeckAPIAccess) import Wire.HashPassword.Interpreter @@ -122,9 +121,12 @@ import Wire.LegalHoldStore.Cassandra (interpretLegalHoldStoreToCassandra) import Wire.LegalHoldStore.Env (LegalHoldEnv (..)) import Wire.NotificationSubsystem.Interpreter (runNotificationSubsystemGundeck) import Wire.ParseException +import Wire.ProposalStore.Cassandra import Wire.RateLimit import Wire.RateLimit.Interpreter import Wire.Rpc +import Wire.Sem.Concurrency +import Wire.Sem.Concurrency.IO import Wire.Sem.Delay import Wire.Sem.Now.IO (nowToIO) import Wire.Sem.Random.IO @@ -142,6 +144,7 @@ type GalleyEffects0 = '[ Input ClientState, Input Hasql.Pool, Input Env, + Input ConversationSubsystemConfig, Error MigrationError, Error InvalidInput, Error ParseException, @@ -160,6 +163,7 @@ type GalleyEffects0 = Embed IO, Error JSONResponse, Resource, + Concurrency 'Unsafe, Final IO ] @@ -170,7 +174,7 @@ validateOptions :: Opts -> IO (Either HttpsUrl (Map Text HttpsUrl)) validateOptions o = do let settings' = view settings o optFanoutLimit = fromIntegral . fromRange $ currentFanoutLimit o - when (settings' ^. maxConvSize > fromIntegral optFanoutLimit) $ + when (settings'._maxConvSize > fromIntegral optFanoutLimit) $ error "setMaxConvSize cannot be > setTruncationLimit" when (settings' ^. maxTeamSize < optFanoutLimit) $ error "setMaxTeamSize cannot be < setTruncationLimit" @@ -299,8 +303,23 @@ evalGalley e = BackendNotificationQueueAccess.local = localUnit, BackendNotificationQueueAccess.requestId = e ^. reqId } + federationAPIAccessConfig = + FederationAPIAccessConfig + { ownDomain = e._options._settings._federationDomain, + federatorEndpoint = e._options._federator, + http2Manager = e._http2Manager, + requestId = e._reqId + } + conversationSubsystemConfig = + ConversationSubsystemConfig + { mlsKeys = e._mlsKeys, + federationProtocols = e._options._settings._federationProtocols, + legalholdDefaults = lh, + maxConvSize = e._options._settings._maxConvSize + } in ExceptT . runFinal @IO + . unsafelyPerformConcurrency . resourceToIOFinal . runError . embedToFinal @IO @@ -317,6 +336,7 @@ evalGalley e = . mapError toResponse . mapError toResponse . logAndMapError toResponse (Text.pack . show) "migration error" + . runInputConst conversationSubsystemConfig . runInputConst e . runInputConst (e ^. hasqlPool) . runInputConst (e ^. cstate) @@ -356,7 +376,7 @@ evalGalley e = . interpretTeamCollaboratorsStoreToPostgres . interpretFireAndForget . BackendNotificationQueueAccess.interpretBackendNotificationQueueAccess backendNotificationQueueAccessEnv - . interpretFederatorAccess + . interpretFederationAPIAccess federationAPIAccessConfig . runRpcWithHttp (e ^. manager) (e ^. reqId) . runGundeckAPIAccess (e ^. options . gundeck) . interpretBrigAccess (e ^. brig) diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index b4fc14043e7..4b20074f9a9 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -21,7 +21,7 @@ module Galley.Effects -- * Effects to access the Intra API BrigAPIAccess, - FederatorAccess, + FederationAPIAccess, SparAPIAccess, -- * External services @@ -65,8 +65,6 @@ import Data.Qualified import Galley.Effects.ClientStore import Galley.Effects.CodeStore import Galley.Effects.CustomBackendStore -import Galley.Effects.FederatorAccess -import Galley.Effects.ProposalStore import Galley.Effects.Queue import Galley.Effects.SearchVisibilityStore import Galley.Effects.TeamFeatureStore @@ -80,12 +78,14 @@ import Polysemy.Error import Polysemy.Input import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client import Wire.API.Team.Feature import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess import Wire.ConversationStore (ConversationStore, MLSCommitLockStore) import Wire.ConversationSubsystem import Wire.ExternalAccess +import Wire.FederationAPIAccess import Wire.FireAndForget import Wire.GundeckAPIAccess import Wire.HashPassword @@ -93,6 +93,7 @@ import Wire.LegalHoldStore import Wire.LegalHoldStore.Env (LegalHoldEnv) import Wire.ListItems import Wire.NotificationSubsystem +import Wire.ProposalStore import Wire.RateLimit import Wire.Rpc import Wire.Sem.Now @@ -118,7 +119,7 @@ type GalleyEffects1 = BrigAPIAccess, GundeckAPIAccess, Rpc, - FederatorAccess, + FederationAPIAccess FederatorClient, BackendNotificationQueueAccess, FireAndForget, TeamCollaboratorsStore, diff --git a/services/galley/src/Galley/Effects/FederatorAccess.hs b/services/galley/src/Galley/Effects/FederatorAccess.hs deleted file mode 100644 index 73ab4ca9844..00000000000 --- a/services/galley/src/Galley/Effects/FederatorAccess.hs +++ /dev/null @@ -1,72 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - --- 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 Galley.Effects.FederatorAccess - ( -- * Federator access effect - FederatorAccess (..), - runFederated, - runFederatedEither, - runFederatedConcurrently, - runFederatedConcurrentlyEither, - runFederatedConcurrentlyBucketsEither, - isFederationConfigured, - ) -where - -import Data.Qualified -import Imports -import Polysemy -import Wire.API.Federation.Client -import Wire.API.Federation.Component -import Wire.API.Federation.Error - -data FederatorAccess m a where - RunFederated :: - (KnownComponent c) => - Remote x -> - FederatorClient c a -> - FederatorAccess m a - RunFederatedEither :: - (KnownComponent c) => - Remote x -> - FederatorClient c a -> - FederatorAccess m (Either FederationError a) - RunFederatedConcurrently :: - (KnownComponent c, Foldable f, Functor f) => - f (Remote x) -> - (Remote [x] -> FederatorClient c a) -> - FederatorAccess m [Remote a] - RunFederatedConcurrentlyEither :: - forall (c :: Component) f a m x. - (KnownComponent c, Foldable f, Functor f) => - f (Remote x) -> - (Remote [x] -> FederatorClient c a) -> - FederatorAccess m [Either (Remote [x], FederationError) (Remote a)] - -- | An action similar to 'RunFederatedConcurrentlyEither', but whose input is - -- already in buckets. The buckets are paired with arbitrary data that affect - -- the payload of the request for each remote backend. - RunFederatedConcurrentlyBucketsEither :: - forall (c :: Component) f a m x. - (KnownComponent c, Foldable f) => - f (Remote x) -> - (Remote x -> FederatorClient c a) -> - FederatorAccess m [Either (Remote x, FederationError) (Remote a)] - IsFederationConfigured :: FederatorAccess m Bool - -makeSem ''FederatorAccess diff --git a/services/galley/src/Galley/Intra/Federator.hs b/services/galley/src/Galley/Intra/Federator.hs deleted file mode 100644 index 6c35754d292..00000000000 --- a/services/galley/src/Galley/Intra/Federator.hs +++ /dev/null @@ -1,121 +0,0 @@ --- 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 Galley.Intra.Federator (interpretFederatorAccess) where - -import Control.Lens -import Data.Bifunctor -import Data.Qualified -import Galley.Cassandra.Util -import Galley.Effects.FederatorAccess (FederatorAccess (..)) -import Galley.Env -import Galley.Env qualified as E -import Galley.Monad -import Galley.Options -import Imports -import Polysemy -import Polysemy.Input -import Polysemy.TinyLog -import UnliftIO -import Wire.API.Federation.Client -import Wire.API.Federation.Error - -interpretFederatorAccess :: - ( Member (Embed IO) r, - Member (Input Env) r, - Member TinyLog r - ) => - Sem (FederatorAccess ': r) a -> - Sem r a -interpretFederatorAccess = interpret $ \case - RunFederated dom rpc -> do - logEffect "FederatorAccess.RunFederated" - embedApp $ runFederated dom rpc - RunFederatedEither dom rpc -> do - logEffect "FederatorAccess.RunFederatedEither" - embedApp $ runFederatedEither dom rpc - RunFederatedConcurrently rs f -> do - logEffect "FederatorAccess.RunFederatedConcurrently" - embedApp $ runFederatedConcurrently rs f - RunFederatedConcurrentlyEither rs f -> do - logEffect "FederatorAccess.RunFederatedConcurrentlyEither" - embedApp $ runFederatedConcurrentlyEither rs f - RunFederatedConcurrentlyBucketsEither rs f -> do - logEffect "FederatorAccess.RunFederatedConcurrentlyBucketsEither" - embedApp $ runFederatedConcurrentlyBucketsEither rs f - IsFederationConfigured -> do - logEffect "FederatorAccess.IsFederationConfigured" - embedApp $ isJust <$> view E.federator - -runFederatedEither :: - Remote x -> - FederatorClient c a -> - App (Either FederationError a) -runFederatedEither (tDomain -> remoteDomain) rpc = do - ownDomain <- view (options . settings . federationDomain) - mfedEndpoint <- view E.federator - mgr <- view http2Manager - rid <- view reqId - case mfedEndpoint of - Nothing -> pure (Left FederationNotConfigured) - Just fedEndpoint -> do - let ce = - FederatorClientEnv - { ceOriginDomain = ownDomain, - ceTargetDomain = remoteDomain, - ceFederator = fedEndpoint, - ceHttp2Manager = mgr, - ceOriginRequestId = rid - } - liftIO . fmap (first FederationCallFailure) $ runFederatorClient ce rpc - -runFederated :: - Remote x -> - FederatorClient c a -> - App a -runFederated dom rpc = - runFederatedEither dom rpc - >>= either (throwIO . federationErrorToWai) pure - -runFederatedConcurrently :: - ( Foldable f, - Functor f - ) => - f (Remote a) -> - (Remote [a] -> FederatorClient c b) -> - App [Remote b] -runFederatedConcurrently xs rpc = - pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> - qualifyAs r <$> runFederated r (rpc r) - -runFederatedConcurrentlyEither :: - (Foldable f, Functor f) => - f (Remote a) -> - (Remote [a] -> FederatorClient c b) -> - App [Either (Remote [a], FederationError) (Remote b)] -runFederatedConcurrentlyEither xs rpc = - pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> - bimap (r,) (qualifyAs r) <$> runFederatedEither r (rpc r) - -runFederatedConcurrentlyBucketsEither :: - (Foldable f) => - f (Remote x) -> - (Remote x -> FederatorClient c b) -> - App [Either (Remote x, FederationError) (Remote b)] -runFederatedConcurrentlyBucketsEither xs rpc = - pooledForConcurrentlyN 8 (toList xs) $ \r -> - bimap (r,) (qualifyAs r) <$> runFederatedEither r (rpc r) From c0c2b352e85e6033b8892588f83707be97c9ae3d Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Thu, 4 Dec 2025 12:04:19 +0100 Subject: [PATCH 07/60] Add multi-ingress domains to SCIM IdPs (#4778) Domains are only added to `WireIdP` when the Z-Host is configured as multi-ingress domain. There can only be one IdP per domain - Adding more is forbidden and results in error responses. (Other processes need an unamabiguous mapping from Domain to IdP.) --- cassandra-schema.cql | 1 + .../2-features/multi-ingress-idp-domains | 9 + .../src/developer/reference/config-options.md | 34 ++ integration/integration.cabal | 1 + integration/test/API/Spar.hs | 40 ++- integration/test/SetupHelpers.hs | 13 +- integration/test/Test/Spar/MultiIngressIdp.hs | 302 ++++++++++++++++++ integration/test/Test/Spar/MultiIngressSSO.hs | 32 +- .../src/Wire/API/Routes/Public/Spar.hs | 4 +- .../src/Wire/API/User/IdentityProvider.hs | 4 +- .../wire-subsystems/src/Wire/UserSubsystem.hs | 6 +- .../test/unit/Wire/MiniBackend.hs | 2 +- services/spar/spar.cabal | 1 + services/spar/src/Spar/API.hs | 91 ++++-- services/spar/src/Spar/Error.hs | 2 + services/spar/src/Spar/Schema/Run.hs | 4 +- services/spar/src/Spar/Schema/V21.hs | 32 ++ .../src/Spar/Sem/IdPConfigStore/Cassandra.hs | 15 +- .../test-integration/Test/Spar/DataSpec.hs | 4 +- services/spar/test-integration/Util/Core.hs | 2 +- 20 files changed, 549 insertions(+), 50 deletions(-) create mode 100644 changelog.d/2-features/multi-ingress-idp-domains create mode 100644 integration/test/Test/Spar/MultiIngressIdp.hs create mode 100644 services/spar/src/Spar/Schema/V21.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 7c4756158c0..b50d79c4e87 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -2050,6 +2050,7 @@ CREATE TABLE spar_test.issuer_idp ( CREATE TABLE spar_test.idp ( idp uuid PRIMARY KEY, api_version int, + domain text, extra_public_keys list, handle text, issuer text, diff --git a/changelog.d/2-features/multi-ingress-idp-domains b/changelog.d/2-features/multi-ingress-idp-domains new file mode 100644 index 00000000000..f8944f2006c --- /dev/null +++ b/changelog.d/2-features/multi-ingress-idp-domains @@ -0,0 +1,9 @@ +When a SAML IdP is created on a multi-ingress domain (implying that +multi-ingress domains are configured in Spar) the domain is added as `domain` +field to that IdP's `extraInfo` (`WireIdP` type in Haskell.) To avoid confusion +in later lookups, at most one IdP can be configured per multi-ingress domain. +If multi-ingress is not configured or it's not configured for the specific +domain, no `domain` field gets added to the IdP. This guards against creating +multiple IdPs and then assigning them to multi-ingress domains. Thus, users who +don't use multi-ingress don't observe any change. This feature only opens the +door to later provide an IdP for a multi-ingress domain. diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 7379ad20ac5..9b6bae2abb7 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -1303,6 +1303,40 @@ clear as there are multiple `ssoUri`s defined. So, the SCIM base URI needs to be set explicitly in `scimBaseUri`. In spar's YAML config file `scimBaseUri` is always required. +#### SAML IdPs + +The multi-ingress configuration also affects SAML IdPs: The multi-ingress +domain (as specified by the internal `Z-Host` header; according to the domain +the `/identity-providers` endpoints are accessed on) is stored in the IdP's +configuration data. It can be observed as field `domain` in responses of +`/identity-providers`. + +For example: +```json +{ + "extraInfo": { + "apiVersion": "WireIdPAPIV2", + "domain": "nginz-https.ernie.example.com", + "handle": "IdP 1", + "oldIssuers": [ + "https://issuer.net/_c4590f08-14da-446b-89d0-fcb46ac8ccf9" + ], + "replacedBy": null, + "team": "ce2c2ade-8b93-4db3-b1d3-44ce1d987ca6" + }, + "id": "ba6afb01-3edf-416c-8561-42e7ecc9b00a", + "metadata": { + "certAuthnResponse": [ + "MIIBOTCBxKADAgECAg4TIFmNatMeqaAE8BWQBTANBgkqhkiG9w0BAQsFADAAMB4XDTI1MDkxODE2MjY1NVoXDTQ1MDkxMzE2MjY1NVowADB6MA0GCSqGSIb3DQEBAQUAA2kAMGYCYQC/KgI1kw9+dXc/XUQ8Q6no9GsT9gX1g3sekVEI7UuxrcHd+Tapzi1T99TdnBDedXCAxGTW6Rwhu3F20j0iAi0neWzi5xv+1KWxK0djzJ0Kxk5AcdDx/Tz+t1Uzd4VXkhECAREwDQYJKoZIhvcNAQELBQADYQAsFrbuDmGZphl9d9VdHyh8a9lIFh3oO5et+tPqFTTRPbbfEewqvtwFWvP9Gf1qgjk0qwKX3GDsFejQf4h94qU1Zf0IE8J/WyIiwEWRvZgAQ9UmqKljmbHKssyNwsl6tTY=" + ], + "issuer": "https://issuer.net/_2ab62f21-44c0-4c60-a115-d05b5129141d", + "requestURI": "https://requri.net/22169147-7d84-4991-9652-d7434986b7d8" + } +} +``` + +There can be at most one IdP per multi-ingress domain. Creating more returns an +error. Though, IdPs can be reconfigured as long as this invariant holds. ### Webapp diff --git a/integration/integration.cabal b/integration/integration.cabal index 9f9bd2c83da..0cdf6972ef1 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -196,6 +196,7 @@ library Test.Search Test.Services Test.Spar + Test.Spar.MultiIngressIdp Test.Spar.MultiIngressSSO Test.Spar.STM Test.Swagger diff --git a/integration/test/API/Spar.hs b/integration/test/API/Spar.hs index 1c1296be366..9fd18a9efdd 100644 --- a/integration/test/API/Spar.hs +++ b/integration/test/API/Spar.hs @@ -161,18 +161,30 @@ mkScimUser scimUserId = -- | https://staging-nginz-https.zinfra.io/v12/api/swagger-ui/#/default/idp-create createIdp :: (HasCallStack, MakesValue user) => user -> SAML.IdPMetadata -> App Response -createIdp user metadata = do - req <- baseRequest user Spar Versioned "/identity-providers" - submit "POST" $ req - & addQueryParams [("api_version", "v2")] - & addXML (fromLT $ SAML.encode metadata) +createIdp = (flip createIdpWithZHost) Nothing + +createIdpWithZHost :: (HasCallStack, MakesValue user) => user -> Maybe String -> SAML.IdPMetadata -> App Response +createIdpWithZHost user mbZHost metadata = do + bReq <- baseRequest user Spar Versioned "/identity-providers" + let req = + bReq + & addQueryParams [("api_version", "v2")] + & addXML (fromLT $ SAML.encode metadata) + & addHeader "Content-Type" "application/xml" + submit "POST" (req & maybe id zHost mbZHost) -- | https://staging-nginz-https.zinfra.io/v7/api/swagger-ui/#/default/idp-update updateIdp :: (HasCallStack, MakesValue user) => user -> String -> SAML.IdPMetadata -> App Response -updateIdp user idpId metadata = do - req <- baseRequest user Spar Versioned $ joinHttpPath ["identity-providers", idpId] - submit "PUT" $ req - & addXML (fromLT $ SAML.encode metadata) +updateIdp = (flip updateIdpWithZHost) Nothing + +updateIdpWithZHost :: (HasCallStack, MakesValue user) => user -> Maybe String -> String -> SAML.IdPMetadata -> App Response +updateIdpWithZHost user mbZHost idpId metadata = do + bReq <- baseRequest user Spar Versioned $ joinHttpPath ["identity-providers", idpId] + let req = + bReq + & addXML (fromLT $ SAML.encode metadata) + & addHeader "Content-Type" "application/xml" + submit "PUT" (req & maybe id zHost mbZHost) -- | https://staging-nginz-https.zinfra.io/v7/api/swagger-ui/#/default/idp-get-all getIdps :: (HasCallStack, MakesValue user) => user -> App Response @@ -180,6 +192,16 @@ getIdps user = do req <- baseRequest user Spar Versioned "/identity-providers" submit "GET" req +getIdp :: (HasCallStack, MakesValue user) => user -> String -> App Response +getIdp user idpId = do + req <- baseRequest user Spar Versioned $ joinHttpPath ["identity-providers", idpId] + submit "GET" req + +deleteIdp :: (HasCallStack, MakesValue user) => user -> String -> App Response +deleteIdp user idpId = do + req <- baseRequest user Spar Versioned $ joinHttpPath ["identity-providers", idpId] + submit "DELETE" req + -- | https://staging-nginz-https.zinfra.io/v7/api/swagger-ui/#/default/sso-team-metadata getSPMetadata :: (HasCallStack, MakesValue domain) => domain -> String -> App Response getSPMetadata = (flip getSPMetadataWithZHost) Nothing diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 1652c1bac60..61f8497d9d5 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -500,10 +500,17 @@ addJSONToFailureContext name ctx action = do registerTestIdPWithMeta :: (HasCallStack, MakesValue owner) => owner -> App Response registerTestIdPWithMeta owner = fst <$> registerTestIdPWithMetaWithPrivateCreds owner -registerTestIdPWithMetaWithPrivateCreds :: (HasCallStack, MakesValue owner) => owner -> App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) -registerTestIdPWithMetaWithPrivateCreds owner = do +registerTestIdPWithMetaWithPrivateCredsForZHost :: + (HasCallStack, MakesValue owner) => + owner -> + Maybe String -> + App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) +registerTestIdPWithMetaWithPrivateCredsForZHost owner mbZhost = do SampleIdP idpmeta pCreds _ _ <- makeSampleIdPMetadata - (,(idpmeta, pCreds)) <$> createIdp owner idpmeta + (,(idpmeta, pCreds)) <$> createIdpWithZHost owner mbZhost idpmeta + +registerTestIdPWithMetaWithPrivateCreds :: (HasCallStack, MakesValue owner) => owner -> App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) +registerTestIdPWithMetaWithPrivateCreds = flip registerTestIdPWithMetaWithPrivateCredsForZHost Nothing updateTestIdpWithMetaWithPrivateCreds :: (HasCallStack, MakesValue owner) => owner -> String -> App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) updateTestIdpWithMetaWithPrivateCreds owner idpId = do diff --git a/integration/test/Test/Spar/MultiIngressIdp.hs b/integration/test/Test/Spar/MultiIngressIdp.hs new file mode 100644 index 00000000000..8294782cb4a --- /dev/null +++ b/integration/test/Test/Spar/MultiIngressIdp.hs @@ -0,0 +1,302 @@ +module Test.Spar.MultiIngressIdp where + +import API.GalleyInternal +import API.Spar +import Control.Lens ((.~), (^.)) +import qualified SAML2.WebSSO.Test.Util as SAML +import qualified SAML2.WebSSO.Types as SAML +import SetupHelpers +import Testlib.Prelude + +ernieZHost :: String +ernieZHost = "nginz-https.ernie.example.com" + +bertZHost :: String +bertZHost = "nginz-https.bert.example.com" + +kermitZHost :: String +kermitZHost = "nginz-https.kermit.example.com" + +-- | Create a `MultiIngressDomainConfig` JSON object with the given @zhost@ +makeSpDomainConfig :: String -> Value +makeSpDomainConfig zhost = + object + [ "spAppUri" .= ("https://webapp." ++ zhost), + "spSsoUri" .= ("https://nginz-https." ++ zhost ++ "/sso"), + "contacts" .= [object ["type" .= ("ContactTechnical" :: String)]] + ] + +testMultiIngressIdpSimpleCase :: (HasCallStack) => App () +testMultiIngressIdpSimpleCase = do + withModifiedBackend + def + { sparCfg = + removeField "saml.spSsoUri" + >=> removeField "saml.spAppUri" + >=> removeField "saml.contacts" + >=> setField + "saml.spDomainConfigs" + ( object + [ ernieZHost .= makeSpDomainConfig ernieZHost, + bertZHost .= makeSpDomainConfig bertZHost + ] + ) + } + $ \domain -> do + (owner, tid, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + + -- Create IdP for one domain + SAML.SampleIdP idpmeta _ _ _ <- SAML.makeSampleIdPMetadata + idpId <- + createIdpWithZHost owner (Just ernieZHost) idpmeta `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + resp.jsonBody %. "id" >>= asString + + getIdp owner idpId `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + + -- Update IdP for another domain + updateIdpWithZHost owner (Just bertZHost) idpId idpmeta `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` bertZHost + + getIdp owner idpId `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` bertZHost + +-- We must guard against domains being filled up with multiple IdPs and then +-- being configured as multi-ingress domains. Then, we'd have multiple IdPs for +-- a multi-ingress domain and cannot decide which one to choose. The solution +-- to this is that unconfigured domains' IdPs store no domain. I.e. the +-- assignment of domains to IdPs begins when the domain is configured as +-- multi-ingress domain. +testUnconfiguredDomain :: (HasCallStack) => App () +testUnconfiguredDomain = forM_ [Nothing, Just kermitZHost] $ \unconfiguredZHost -> do + withModifiedBackend + def + { sparCfg = + removeField "saml.spSsoUri" + >=> removeField "saml.spAppUri" + >=> removeField "saml.contacts" + >=> setField + "saml.spDomainConfigs" + (object [ernieZHost .= makeSpDomainConfig ernieZHost]) + } + $ \domain -> do + (owner, tid, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + + SAML.SampleIdP idpmeta1 _ _ _ <- SAML.makeSampleIdPMetadata + idpId1 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + resp.jsonBody %. "id" >>= asString + + -- From configured domain to unconfigured -> no multi-ingress domain + updateIdpWithZHost owner (unconfiguredZHost) idpId1 idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + getIdp owner idpId1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + -- From unconfigured back to configured -> add multi-ingress domain + updateIdpWithZHost owner (Just ernieZHost) idpId1 idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + + getIdp owner idpId1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + + -- Create unconfigured -> no multi-ingress domain + SAML.SampleIdP idpmeta2 _ _ _ <- SAML.makeSampleIdPMetadata + idpId2 <- + createIdpWithZHost owner (unconfiguredZHost) idpmeta2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + getIdp owner idpId2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + -- Create a second unconfigured -> no multi-ingress domain + SAML.SampleIdP idpmeta3 _ _ _ <- SAML.makeSampleIdPMetadata + idpId3 <- + createIdpWithZHost owner (unconfiguredZHost) idpmeta3 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + getIdp owner idpId3 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + +testMultiIngressAtMostOneIdPPerDomain :: (HasCallStack) => App () +testMultiIngressAtMostOneIdPPerDomain = do + withModifiedBackend + def + { sparCfg = + removeField "saml.spSsoUri" + >=> removeField "saml.spAppUri" + >=> removeField "saml.contacts" + >=> setField + "saml.spDomainConfigs" + ( object + [ ernieZHost .= makeSpDomainConfig ernieZHost, + bertZHost .= makeSpDomainConfig bertZHost + ] + ) + } + $ \domain -> do + (owner, tid, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + + SAML.SampleIdP idpmeta1 _ _ _ <- SAML.makeSampleIdPMetadata + idpId1 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "id" >>= asString + + -- Creating a second IdP for the same domain -> failure + SAML.SampleIdP idpmeta2 _ _ _ <- SAML.makeSampleIdPMetadata + _idpId2 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + -- Create an IdP for one domain and update it to another that already has one -> failure + SAML.SampleIdP idpmeta3 _ _ _ <- SAML.makeSampleIdPMetadata + idpId3 <- + createIdpWithZHost owner (Just bertZHost) idpmeta2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "id" >>= asString + + updateIdpWithZHost owner (Just ernieZHost) idpId3 idpmeta3 + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + -- Create an IdP with no domain and update it to a domain that already has one -> failure + SAML.SampleIdP idpmeta4 _ _ _ <- SAML.makeSampleIdPMetadata + idpId4 <- + createIdpWithZHost owner Nothing idpmeta4 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "id" >>= asString + + updateIdpWithZHost owner (Just ernieZHost) idpId4 idpmeta4 + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + -- Updating an IdP itself should still work + updateIdpWithZHost + owner + (Just ernieZHost) + idpId1 + -- The edIssuer needs to stay unchanged. Otherwise, deletion will fail + -- with a 404 (see bug https://wearezeta.atlassian.net/browse/WPB-20407) + (idpmeta2 & SAML.edIssuer .~ (idpmeta1 ^. SAML.edIssuer)) + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + + -- After deletion of the IdP of a domain, a new one can be created + deleteIdp owner idpId1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 204 + + SAML.SampleIdP idpmeta5 _ _ _ <- SAML.makeSampleIdPMetadata + idpId5 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta5 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + resp.jsonBody %. "id" >>= asString + + -- After deletion of the IdP of a domain, one can be moved from another domain + SAML.SampleIdP idpmeta6 _ _ _ <- SAML.makeSampleIdPMetadata + createIdpWithZHost owner (Just bertZHost) idpmeta6 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + deleteIdp owner idpId3 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 204 + + idpId6 <- + createIdpWithZHost owner (Just bertZHost) idpmeta6 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` bertZHost + resp.jsonBody %. "id" >>= asString + + updateIdpWithZHost owner (Just ernieZHost) idpId6 idpmeta6 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + deleteIdp owner idpId5 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 204 + + updateIdpWithZHost owner (Just ernieZHost) idpId6 idpmeta6 + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + +-- We only record the domain for multi-ingress setups. +testNonMultiIngressSetupsCanHaveMoreIdPsPerDomain :: (HasCallStack) => App () +testNonMultiIngressSetupsCanHaveMoreIdPsPerDomain = do + (owner, tid, _) <- createTeam OwnDomain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + + -- With Z-Host header + SAML.SampleIdP idpmeta1 _ _ _ <- SAML.makeSampleIdPMetadata + idpId1 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + SAML.SampleIdP idpmeta2 _ _ _ <- SAML.makeSampleIdPMetadata + idpId2 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + SAML.SampleIdP idpmeta3 _ _ _ <- SAML.makeSampleIdPMetadata + updateIdpWithZHost owner (Just ernieZHost) idpId1 idpmeta3 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + SAML.SampleIdP idpmeta4 _ _ _ <- SAML.makeSampleIdPMetadata + updateIdpWithZHost owner (Just ernieZHost) idpId2 idpmeta4 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + -- Without Z-Host header + SAML.SampleIdP idpmeta5 _ _ _ <- SAML.makeSampleIdPMetadata + idpId5 <- + createIdpWithZHost owner Nothing idpmeta5 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + SAML.SampleIdP idpmeta6 _ _ _ <- SAML.makeSampleIdPMetadata + idpId6 <- + createIdpWithZHost owner Nothing idpmeta6 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + SAML.SampleIdP idpmeta7 _ _ _ <- SAML.makeSampleIdPMetadata + updateIdpWithZHost owner Nothing idpId5 idpmeta7 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + SAML.SampleIdP idpmeta8 _ _ _ <- SAML.makeSampleIdPMetadata + updateIdpWithZHost owner Nothing idpId6 idpmeta8 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null diff --git a/integration/test/Test/Spar/MultiIngressSSO.hs b/integration/test/Test/Spar/MultiIngressSSO.hs index 5a9ef87aef1..0f5829e6665 100644 --- a/integration/test/Test/Spar/MultiIngressSSO.hs +++ b/integration/test/Test/Spar/MultiIngressSSO.hs @@ -35,8 +35,34 @@ import qualified Text.XML as XML import qualified Text.XML.Cursor as XML import qualified Text.XML.DSig as SAML -testMultiIngressSSO :: (HasCallStack) => App () -testMultiIngressSSO = do +-- | Test multi-ingress SSO with an IdP that is not bound to a domain. +-- +-- The IdP is created via a non-multi-ingress way/domain. It is valid for all +-- domains - no matter if they are configured as multi-ingress domains or not. +-- However, the SP must be consistent in the communication: If the SAML login +-- flow was started on one domain, it must return to exactly this domain. +testMultiIngressSSOGeneralIdp :: (HasCallStack) => App () +testMultiIngressSSOGeneralIdp = multiIngressSSOCommonTest (const . registerTestIdPWithMetaWithPrivateCreds) + +-- | Test multi-ingress SSO with an IdP that is bound to a domain. +-- +-- The IdP is created on a multi-ingress domain. The details of managing +-- multi-ingress IdPs are covered in `Test.Spar.MultiIngressIdp`. Here we want +-- to test that logins are possible with such an IdP, ensuring we haven't +-- broken basic functionality. +testMultiIngressSSODomainBoundIdp :: (HasCallStack) => App () +testMultiIngressSSODomainBoundIdp = multiIngressSSOCommonTest registerTestIdPWithMetaWithPrivateCredsForZHost + +multiIngressSSOCommonTest :: + (HasCallStack) => + ( forall owner. + (HasCallStack, MakesValue owner) => + owner -> + Maybe String -> + App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) + ) -> + App () +multiIngressSSOCommonTest registerTestIdPWithMetaWithPrivateCredsFn = do let ernieZHost = "nginz-https.ernie.example.com" bertZHost = "nginz-https.bert.example.com" kermitZHost = "nginz-https.kermit.example.com" @@ -69,7 +95,7 @@ testMultiIngressSSO = do (owner, tid, _) <- createTeam domain 1 void $ setTeamFeatureStatus owner tid "sso" "enabled" - (idp, idpMeta) <- registerTestIdPWithMetaWithPrivateCreds owner + (idp, idpMeta) <- registerTestIdPWithMetaWithPrivateCredsFn owner (Just ernieZHost) idpId <- asString $ idp.json %. "id" ernieEmail <- ("ernie@" <>) <$> randomDomain diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index 09020317e3a..6a40c1330b7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -136,8 +136,8 @@ type APIIDP = :<|> Named "idp-get-raw" (ZOptUser :> IdpGetRaw) :<|> Named "idp-get-all" (ZOptUser :> IdpGetAll) :<|> Named "idp-create@v7" (Until 'V8 :> AuthProtect "TeamAdmin" :> IdpCreate) -- (change is semantic, see handler) - :<|> Named "idp-create" (From 'V8 :> AuthProtect "TeamAdmin" :> IdpCreate) - :<|> Named "idp-update" (ZOptUser :> IdpUpdate) + :<|> Named "idp-create" (From 'V8 :> AuthProtect "TeamAdmin" :> ZHostOpt :> IdpCreate) + :<|> Named "idp-update" (ZOptUser :> ZHostOpt :> IdpUpdate) :<|> Named "idp-delete" (ZOptUser :> IdpDelete) type IdpGetRaw = Capture "id" SAML.IdPId :> "raw" :> Get '[RawXML] RawIdPMetadata diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index 3241f924475..b6ffbd71299 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -60,6 +60,7 @@ import SAML2.WebSSO qualified as SAML import SAML2.WebSSO.Test.Arbitrary () import SAML2.WebSSO.Types.TH (deriveJSONOptions) import Servant.API as Servant hiding (MkLink, URI (..)) +import Wire.API.Routes.Public (ZHostValue) import Wire.API.User.Orphans (samlSchemaOptions) import Wire.API.Util.Aeson (defaultOptsDropChar) import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) @@ -82,7 +83,8 @@ data WireIdP = WireIdP -- with the @"replaces"@ query parameter, and it is used to decide whether users not -- existing on this IdP can be auto-provisioned (if 'isJust', they can't). _replacedBy :: Maybe SAML.IdPId, - _handle :: IdPHandle + _handle :: IdPHandle, + _domain :: Maybe ZHostValue } deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform WireIdP) diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 3ca9df5e115..ee6f58cff40 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -50,7 +50,7 @@ import Wire.API.Team.Feature import Wire.API.Team.Member (IsPerm (..), TeamMember) import Wire.API.User import Wire.API.User.Activation -import Wire.API.User.IdentityProvider hiding (team) +import Wire.API.User.IdentityProvider hiding (domain, team) import Wire.API.User.Search import Wire.ActivationCodeStore import Wire.Arbitrary @@ -301,11 +301,11 @@ requestEmailChange lusr email allowScim = do ) => Sem r' () guardBlockedDomainEmail = do - domain <- + eDomain <- either (throwGuardFailed . InvalidDomain) pure $ emailDomain email blocked <- blockedDomains <$> input - when (domain `elem` blocked) $ + when (eDomain `elem` blocked) $ throw UserSubsystemBlockedDomain -- | Prepare changing the email (checking a number of invariants). diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index aa521765bf0..204fc5ed737 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -733,7 +733,7 @@ miniFederationAPIAccess :: Sem r a miniFederationAPIAccess online = do let runner :: FederatedActionRunner MiniFederationMonad r - runner domain rpc = pure . Right $ runMiniFederation domain online rpc + runner ownDomain rpc = pure . Right $ runMiniFederation ownDomain online rpc interpret \case RunFederatedEither remote rpc -> if isJust (M.lookup (qDomain $ tUntagged remote) online) diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 4117e731c14..c9637fca4f4 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -43,6 +43,7 @@ library Spar.Schema.V19 Spar.Schema.V2 Spar.Schema.V20 + Spar.Schema.V21 Spar.Schema.V3 Spar.Schema.V4 Spar.Schema.V5 diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 6224146fb27..58e313646e6 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -55,6 +55,7 @@ import Data.Domain import Data.HavePendingInvitations import Data.Id import Data.List.NonEmpty (NonEmpty) +import qualified Data.Map as Map import Data.Proxy import Data.Range import Data.Text.Encoding.Error @@ -109,6 +110,7 @@ import System.Logger (Msg) import qualified URI.ByteString as URI import Wire.API.Routes.Internal.Spar import Wire.API.Routes.Named +import Wire.API.Routes.Public (ZHostValue) import Wire.API.Routes.Public.Spar import Wire.API.Team.Member (HiddenPerm (CreateUpdateDeleteIdp, ReadIdp)) import Wire.API.User @@ -174,7 +176,7 @@ api :: ServerT SparAPI (Sem r) api opts = apiSSO opts - :<|> apiIDP + :<|> apiIDP opts :<|> apiScim :<|> apiINTERNAL @@ -219,14 +221,15 @@ apiIDP :: Member SAMLUserStore r, Member (Error SparError) r ) => + Opts -> ServerT APIIDP (Sem r) -apiIDP = +apiIDP opts = Named @"idp-get" idpGet -- get, json, captures idp id :<|> Named @"idp-get-raw" idpGetRaw -- get, raw xml, capture idp id :<|> Named @"idp-get-all" idpGetAll -- get, json - :<|> Named @"idp-create@v7" idpCreateV7 - :<|> Named @"idp-create" idpCreate -- post, created - :<|> Named @"idp-update" idpUpdate -- put, okay + :<|> Named @"idp-create@v7" (idpCreateV7 opts.saml) + :<|> Named @"idp-create" (idpCreate opts.saml) -- post, created + :<|> Named @"idp-update" (idpUpdate opts.saml) -- put, okay :<|> Named @"idp-delete" idpDelete -- delete, no content apiINTERNAL :: @@ -594,7 +597,7 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co -- to be deleted in its old issuers list, but it's tricky to avoid race conditions, and -- there is little to be gained here: we only use old issuers to find users that have not -- been migrated yet, and if an old user points to a deleted idp, it just means that we - -- won't find any users to migrate. still, doesn't hurt mucht to look either. so we + -- won't find any users to migrate. still, doesn't hurt much to look either. so we -- leave old issuers dangling for now. updateReplacingIdP :: IdP -> Sem r () @@ -631,22 +634,46 @@ idpCreate :: Member IdPRawMetadataStore r, Member (Error SparError) r ) => + SAML.Config -> TeamId -> + Maybe ZHostValue -> IdPMetadataInfo -> Maybe SAML.IdPId -> Maybe WireIdPAPIVersion -> Maybe (Range 1 32 Text) -> Sem r IdP -idpCreate tid (IdPMetadataValue rawIdpMetadata idpmeta) mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) mHandle = withDebugLog "idpCreateXML" (Just . show . (^. SAML.idpId)) $ do +idpCreate samlConfig tid uncheckedMbHost (IdPMetadataValue rawIdpMetadata idpmeta) mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) mHandle = withDebugLog "idpCreateXML" (Just . show . (^. SAML.idpId)) $ do + let mbHost = filterMultiIngressZHost (samlConfig._cfgDomainConfigs) uncheckedMbHost GalleyAccess.assertSSOEnabled tid + guardMultiIngressDuplicateDomain tid mbHost idp <- maybe (IdPConfigStore.newHandle tid) (pure . IdPHandle . fromRange) mHandle - >>= validateNewIdP apiversion idpmeta tid mReplaces + >>= validateNewIdP apiversion idpmeta tid mReplaces mbHost IdPRawMetadataStore.store (idp ^. SAML.idpId) rawIdpMetadata IdPConfigStore.insertConfig idp forM_ mReplaces $ \replaces -> IdPConfigStore.setReplacedBy (Replaced replaces) (Replacing (idp ^. SAML.idpId)) pure idp + where + -- Ensure that the domain is not in use by an existing IDP + guardMultiIngressDuplicateDomain :: + ( Member (Error SparError) r, + Member IdPConfigStore r + ) => + TeamId -> + Maybe ZHostValue -> + Sem r () + guardMultiIngressDuplicateDomain _teamId Nothing = pure () + guardMultiIngressDuplicateDomain teamId (Just zHost) = do + idps <- IdPConfigStore.getConfigsByTeam teamId + let domains = idps ^.. traverse . SAML.idpExtraInfo . domain . _Just + when (zHost `elem` domains) $ + throwSparSem SparIdPDomainInUse + +-- | Only return a ZHost when multi-ingress is configured and the host value is a configured domain +filterMultiIngressZHost :: Either SAML.MultiIngressDomainConfig (Map Domain SAML.MultiIngressDomainConfig) -> Maybe ZHostValue -> Maybe ZHostValue +filterMultiIngressZHost (Right domainMap) (Just zHost) | (Domain zHost) `Map.member` domainMap = Just zHost +filterMultiIngressZHost _ _ = Nothing idpCreateV7 :: ( Member Random r, @@ -658,15 +685,16 @@ idpCreateV7 :: Member IdPRawMetadataStore r, Member (Error SparError) r ) => + SAML.Config -> TeamId -> IdPMetadataInfo -> Maybe SAML.IdPId -> Maybe WireIdPAPIVersion -> Maybe (Range 1 32 Text) -> Sem r IdP -idpCreateV7 tid idpmeta mReplaces mApiversion mHandle = do +idpCreateV7 samlConfig tid idpmeta mReplaces mApiversion mHandle = do assertNoScimOrNoIdP - idpCreate tid idpmeta mReplaces mApiversion mHandle + idpCreate samlConfig tid Nothing idpmeta mReplaces mApiversion mHandle where -- In teams with a scim access token, only one IdP is allowed. The reason is that scim user -- data contains no information about the idp issuer, only the user name, so no valid saml @@ -716,9 +744,10 @@ validateNewIdP :: SAML.IdPMetadata -> TeamId -> Maybe SAML.IdPId -> + Maybe ZHostValue -> IdPHandle -> m IdP -validateNewIdP apiversion _idpMetadata teamId mReplaces idHandle = withDebugLog "validateNewIdP" (Just . show . (^. SAML.idpId)) $ do +validateNewIdP apiversion _idpMetadata teamId mReplaces idpDomain idHandle = withDebugLog "validateNewIdP" (Just . show . (^. SAML.idpId)) $ do _idpId <- SAML.IdPId <$> Random.uuid oldIssuersList :: [SAML.Issuer] <- case mReplaces of Nothing -> pure [] @@ -726,7 +755,7 @@ validateNewIdP apiversion _idpMetadata teamId mReplaces idHandle = withDebugLog idp <- IdPConfigStore.getConfig replaces pure $ (idp ^. SAML.idpMetadata . SAML.edIssuer) : (idp ^. SAML.idpExtraInfo . oldIssuers) let requri = _idpMetadata ^. SAML.edRequestURI - _idpExtraInfo = WireIdP teamId (Just apiversion) oldIssuersList Nothing idHandle + _idpExtraInfo = WireIdP teamId (Just apiversion) oldIssuersList Nothing idHandle idpDomain enforceHttps requri mbIdp <- case apiversion of WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe (_idpMetadata ^. SAML.edIssuer) @@ -758,12 +787,16 @@ idpUpdate :: Member IdPRawMetadataStore r, Member (Error SparError) r ) => + SAML.Config -> Maybe UserId -> + Maybe ZHostValue -> IdPMetadataInfo -> SAML.IdPId -> Maybe (Range 1 32 Text) -> Sem r IdP -idpUpdate zusr (IdPMetadataValue raw xml) = idpUpdateXML zusr raw xml +idpUpdate samlConfig zusr uncheckedMbHost (IdPMetadataValue raw xml) = + let mbHost = filterMultiIngressZHost (samlConfig._cfgDomainConfigs) uncheckedMbHost + in idpUpdateXML zusr mbHost raw xml idpUpdateXML :: ( Member Random r, @@ -775,29 +808,51 @@ idpUpdateXML :: Member (Error SparError) r ) => Maybe UserId -> + Maybe ZHostValue -> Text -> SAML.IdPMetadata -> SAML.IdPId -> Maybe (Range 1 32 Text) -> Sem r IdP -idpUpdateXML zusr raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML" (Just . show . (^. SAML.idpId)) $ do +idpUpdateXML zusr mDomain raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML" (Just . show . (^. SAML.idpId)) $ do (teamid, idp) <- validateIdPUpdate zusr idpmeta idpid GalleyAccess.assertSSOEnabled teamid + guardMultiIngressDuplicateDomain teamid mDomain idpid IdPRawMetadataStore.store (idp ^. SAML.idpId) raw let idp' :: IdP = case mHandle of Just idpHandle -> idp & (SAML.idpExtraInfo . handle) .~ IdPHandle (fromRange idpHandle) Nothing -> idp + idp'' :: IdP = idp' & (SAML.idpExtraInfo . domain) .~ mDomain -- (if raw metadata is stored and then spar goes out, raw metadata won't match the -- structured idp config. since this will lead to a 5xx response, the client is expected to -- try again, which would clean up cassandra state.) - IdPConfigStore.insertConfig idp' + IdPConfigStore.insertConfig idp'' -- if the IdP issuer is updated, the old issuer must be removed explicitly. -- if this step is ommitted (due to a crash) resending the update request should fix the inconsistent state. - let mbteamid = case fromMaybe defWireIdPAPIVersion $ idp' ^. SAML.idpExtraInfo . apiVersion of + let mbteamid = case fromMaybe defWireIdPAPIVersion $ idp'' ^. SAML.idpExtraInfo . apiVersion of WireIdPAPIV1 -> Nothing WireIdPAPIV2 -> Just teamid - forM_ (idp' ^. SAML.idpExtraInfo . oldIssuers) (flip IdPConfigStore.deleteIssuer mbteamid) - pure idp' + forM_ (idp'' ^. SAML.idpExtraInfo . oldIssuers) (flip IdPConfigStore.deleteIssuer mbteamid) + pure idp'' + where + -- Ensure that the domain is not in use by an existing IDP + guardMultiIngressDuplicateDomain :: + ( Member (Error SparError) r, + Member IdPConfigStore r + ) => + TeamId -> + Maybe ZHostValue -> + SAML.IdPId -> + Sem r () + guardMultiIngressDuplicateDomain _teamId Nothing _ = pure () + guardMultiIngressDuplicateDomain teamId (Just zHost) idpId = do + idps <- IdPConfigStore.getConfigsByTeam teamId + let otherIdpsOnSameDomain = + any + (\idp -> (idp ^. SAML.idpExtraInfo . domain) == (Just zHost) && (idp ^. SAML.idpId) /= idpId) + idps + when otherIdpsOnSameDomain $ + throwSparSem SparIdPDomainInUse -- | Check that: idp id is valid; calling user is admin in that idp's home team; team id in -- new metainfo doesn't change; new issuer (if changed) is not in use anywhere else (except as diff --git a/services/spar/src/Spar/Error.hs b/services/spar/src/Spar/Error.hs index 36e0d823c18..526b4ce8713 100644 --- a/services/spar/src/Spar/Error.hs +++ b/services/spar/src/Spar/Error.hs @@ -119,6 +119,7 @@ data SparCustomError | SparSomeHttpError HttpError | -- | All errors returned from SCIM handlers are wrapped into 'SparScimError' SparScimError Scim.ScimError + | SparIdPDomainInUse deriving (Eq, Show) data SparProvisioningMoreThanOneIdP @@ -231,6 +232,7 @@ renderSparError (SAML.CustomError (IdpDbError AttemptToGetV2IssuerViaV1API)) = S renderSparError (SAML.CustomError (IdpDbError IdpNonUnique)) = StdError $ Wai.mkError status409 "idp-non-unique" "We have found multiple IdPs with the same issuer. Please contact customer support." renderSparError (SAML.CustomError (IdpDbError IdpWrongTeam)) = StdError $ Wai.mkError status409 "idp-wrong-team" "The IdP is not part of this team." renderSparError (SAML.CustomError (IdpDbError IdpNotFound)) = renderSparError (SAML.CustomError (SparIdPNotFound "")) +renderSparError (SAML.CustomError SparIdPDomainInUse) = StdError $ Wai.mkError status409 "idp-duplicate-domain-for-team" "This team already has an IdP configured for this domain." -- Errors related to provisioning renderSparError (SAML.CustomError (SparProvisioningMoreThanOneIdP msg)) = StdError $ Wai.mkError status400 "more-than-one-idp" do diff --git a/services/spar/src/Spar/Schema/Run.hs b/services/spar/src/Spar/Schema/Run.hs index 46bf1e5bd5f..2ddb825c3ce 100644 --- a/services/spar/src/Spar/Schema/Run.hs +++ b/services/spar/src/Spar/Schema/Run.hs @@ -35,6 +35,7 @@ import qualified Spar.Schema.V18 as V18 import qualified Spar.Schema.V19 as V19 import qualified Spar.Schema.V2 as V2 import qualified Spar.Schema.V20 as V20 +import qualified Spar.Schema.V21 as V21 import qualified Spar.Schema.V3 as V3 import qualified Spar.Schema.V4 as V4 import qualified Spar.Schema.V5 as V5 @@ -82,7 +83,8 @@ migrations = V17.migration, V18.migration, V19.migration, - V20.migration + V20.migration, + V21.migration -- TODO: Add a migration that removes unused fields -- (we don't want to risk running a migration which would -- effectively break the currently deployed spar service) diff --git a/services/spar/src/Spar/Schema/V21.hs b/services/spar/src/Spar/Schema/V21.hs new file mode 100644 index 00000000000..aaff2080483 --- /dev/null +++ b/services/spar/src/Spar/Schema/V21.hs @@ -0,0 +1,32 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 Spar.Schema.V21 + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 21 "Add domain column to idp table" $ do + schema' + [r| + ALTER TABLE idp ADD (domain text); + |] diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs index bec55944aa9..ca771dbab78 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs @@ -38,6 +38,7 @@ import Spar.Data.Instances () import Spar.Error import Spar.Sem.IdPConfigStore (IdPConfigStore (..), Replaced (..), Replacing (..)) import URI.ByteString +import Wire.API.Routes.Public (ZHostValue) import Wire.API.User.IdentityProvider hiding (apiVersion, oldIssuers, replacedBy, team) import qualified Wire.API.User.IdentityProvider as IP import {- instance Cql SAML.IdPId -} Wire.DomainRegistrationStore.Cassandra () @@ -67,7 +68,7 @@ idPToCassandra = ClearReplacedBy r -> embed @m $ clearReplacedBy r DeleteIssuer i t -> embed @m $ deleteIssuer i t -type IdPConfigRow = (SAML.IdPId, SAML.Issuer, URI, SignedCertificate, [SignedCertificate], TeamId, Maybe WireIdPAPIVersion, [SAML.Issuer], Maybe SAML.IdPId, Maybe Text) +type IdPConfigRow = (SAML.IdPId, SAML.Issuer, URI, SignedCertificate, [SignedCertificate], TeamId, Maybe WireIdPAPIVersion, [SAML.Issuer], Maybe SAML.IdPId, Maybe Text, Maybe ZHostValue) insertIdPConfig :: forall m. @@ -91,7 +92,8 @@ insertIdPConfig idp = do idp ^. SAML.idpExtraInfo . IP.apiVersion, idp ^. SAML.idpExtraInfo . IP.oldIssuers, idp ^. SAML.idpExtraInfo . IP.replacedBy, - Just (unIdPHandle $ idp ^. SAML.idpExtraInfo . handle) + Just (unIdPHandle $ idp ^. SAML.idpExtraInfo . handle), + idp ^. SAML.idpExtraInfo . IP.domain ) addPrepQuery byIssuer @@ -119,7 +121,7 @@ insertIdPConfig idp = do getAllIdPsByIssuerUnsafe issuer >>= mapM_ (failIfNot thisVersion) ins :: PrepQuery W IdPConfigRow () - ins = "INSERT INTO idp (idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by, handle) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + ins = "INSERT INTO idp (idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by, handle, domain) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" -- FUTUREWORK: migrate `spar.issuer_idp` away, `spar.issuer_idp_v2` is enough. byIssuer :: PrepQuery W (SAML.Issuer, TeamId, SAML.IdPId) () @@ -177,18 +179,19 @@ getIdPConfig idpid = do apiVersion, oldIssuers, replacedBy, - mHandle + mHandle, + idpDomain ) = do let _edCertAuthnResponse = certsHead NL.:| certsTail _idpMetadata = SAML.IdPMetadata {..} - _idpExtraInfo = WireIdP teamId apiVersion oldIssuers replacedBy (mkHandle mHandle) + _idpExtraInfo = WireIdP teamId apiVersion oldIssuers replacedBy (mkHandle mHandle) idpDomain pure $ SAML.IdPConfig {..} mbidp <- traverse toIdp =<< retry x1 (query1 sel $ params LocalQuorum (Identity idpid)) maybe (throwError IdpNotFound) pure mbidp where sel :: PrepQuery R (Identity SAML.IdPId) IdPConfigRow - sel = "SELECT idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by, handle FROM idp WHERE idp = ?" + sel = "SELECT idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by, handle, domain FROM idp WHERE idp = ?" selTid :: PrepQuery R (Identity SAML.IdPId) (Identity (Maybe TeamId)) selTid = "SELECT team FROM idp WHERE idp = ?" diff --git a/services/spar/test-integration/Test/Spar/DataSpec.hs b/services/spar/test-integration/Test/Spar/DataSpec.hs index 49b5db4690b..ec4b589145c 100644 --- a/services/spar/test-integration/Test/Spar/DataSpec.hs +++ b/services/spar/test-integration/Test/Spar/DataSpec.hs @@ -201,14 +201,14 @@ spec = do it "getIdPConfigsByTeam works" $ do skipIdPAPIVersions [WireIdPAPIV1] teamid <- nextWireId - idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid Nothing [] Nothing (IdPHandle "IdP 1") + idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid Nothing [] Nothing (IdPHandle "IdP 1") Nothing () <- runSpar $ IdPEffect.insertConfig idp idps <- runSpar $ IdPEffect.getConfigsByTeam teamid liftIO $ idps `shouldBe` [idp] it "deleteIdPConfig works" $ do teamid <- nextWireId idpApiVersion <- asks (^. teWireIdPAPIVersion) - idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid (Just idpApiVersion) [] Nothing (IdPHandle "IdP 1") + idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid (Just idpApiVersion) [] Nothing (IdPHandle "IdP 1") Nothing () <- runSpar $ IdPEffect.insertConfig idp do midp <- runSpar $ IdPEffect.getConfig (idp ^. idpId) diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index c7b701ae801..f6c8e25d73c 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -545,7 +545,7 @@ nextWireId :: (MonadIO m) => m (Id a) nextWireId = Id <$> liftIO UUID.nextRandom nextWireIdP :: (MonadIO m) => WireIdPAPIVersion -> m WireIdP -nextWireIdP version = WireIdP <$> iid <*> pure (Just version) <*> pure [] <*> pure Nothing <*> idpHandle +nextWireIdP version = WireIdP <$> iid <*> pure (Just version) <*> pure [] <*> pure Nothing <*> idpHandle <*> pure Nothing where iid = Id <$> liftIO UUID.nextRandom idpHandle = iid <&> IdPHandle . pack . show From d52833f63f76e79f0ef62a676e140a189e262eb9 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 5 Dec 2025 15:10:35 +0100 Subject: [PATCH 08/60] WPB-22168 backend new feature flag cells internal (#4889) --- changelog.d/2-features/WPB-22168 | 1 + charts/galley/templates/configmap.yaml | 4 + charts/galley/values.yaml | 15 ++ .../src/developer/reference/config-options.md | 22 +++ hack/helm_vars/wire-server/values.yaml.gotmpl | 11 ++ integration/integration.cabal | 1 + integration/test/Test/FeatureFlags/Cells.hs | 18 +-- .../test/Test/FeatureFlags/CellsInternal.hs | 113 +++++++++++++ integration/test/Test/FeatureFlags/Util.hs | 14 +- libs/galley-types/src/Galley/Types/Teams.hs | 7 + libs/wire-api/src/Wire/API/Error/Galley.hs | 2 + .../Wire/API/Routes/Public/Galley/Feature.hs | 1 + libs/wire-api/src/Wire/API/Team/Feature.hs | 151 +++++++++++++++++- services/galley/galley.integration.yaml | 11 ++ services/galley/src/Galley/API/Internal.hs | 1 + .../galley/src/Galley/API/Public/Feature.hs | 1 + .../galley/src/Galley/API/Teams/Features.hs | 9 ++ .../src/Galley/API/Teams/Features/Get.hs | 2 + tools/stern/src/Stern/API.hs | 2 + tools/stern/src/Stern/API/Routes.hs | 2 + tools/stern/test/integration/API.hs | 23 +++ 21 files changed, 395 insertions(+), 16 deletions(-) create mode 100644 changelog.d/2-features/WPB-22168 create mode 100644 integration/test/Test/FeatureFlags/CellsInternal.hs diff --git a/changelog.d/2-features/WPB-22168 b/changelog.d/2-features/WPB-22168 new file mode 100644 index 00000000000..a1b41959ce2 --- /dev/null +++ b/changelog.d/2-features/WPB-22168 @@ -0,0 +1 @@ +New team feature config `cellsInternal` diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 52ba50b6181..5c9072a5169 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -186,6 +186,10 @@ data: cells: {{- toYaml .settings.featureFlags.cells | nindent 10 }} {{- end }} + {{- if .settings.featureFlags.cellsInternal }} + cellsInternal: + {{- toYaml .settings.featureFlags.cellsInternal | nindent 10 }} + {{- end }} {{- if .settings.featureFlags.allowedGlobalOperations }} allowedGlobalOperations: {{- toYaml .settings.featureFlags.allowedGlobalOperations | nindent 10 }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index af7d16e04e1..5c8aeee2728 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -215,6 +215,21 @@ config: allowed_to_create_channels: team-members allowed_to_open_channels: team-members lockStatus: locked + cells: + defaults: + status: enabled + lockStatus: unlocked + cellsInternal: + defaults: + status: enabled + lockStatus: unlocked + config: + backend: + url: https://cells-beta.wire.com + collabora: + edition: COOL + storage: + teamQuotaBytes: "1000000000000" allowedGlobalOperations: status: enabled config: diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 9b6bae2abb7..cb123a3b5a6 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -581,6 +581,28 @@ cells: lockStatus: locked ``` +### Cells Interna + +Cells configuration is intentionally split: `cells` is controlled by the team admin, while `cellsInternal` is set by the site operator/customer support via the internal API only. For `cellsInternal`, the `status` and `lockStatus` fields are *required* to be set to `enabled` and `unlocked` respectively, as enforced by validation logic. Failure to set these values will result in a configuration error. This block holds the backend URL, Collabora edition, and a storage quota. The quota must be provided as a positive decimal string that fits in `Int64` bytes. + +```yaml +# galley.yaml +config: + settings: + featureFlags: + cellsInternal: + defaults: + status: enabled + lockStatus: unlocked + config: + backend: + url: https://cells-beta.wire.com + collabora: + edition: COOL + storage: + teamQuotaBytes: "1000000000000" # 1 TB +``` + ### Allowed Global Operations `allowedGlobalOperations` currently supports a single value `mlsConversationReset` which determines if it is allowed to reset MLS conversations by the client. diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index b47848bfffd..ad1501c8d86 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -368,6 +368,17 @@ galley: defaults: status: enabled lockStatus: unlocked + cellsInternal: + defaults: + status: enabled + lockStatus: unlocked + config: + backend: + url: https://cells-beta.wire.com + collabora: + edition: COOL + storage: + teamQuotaBytes: "1000000000000" allowedGlobalOperations: status: enabled config: diff --git a/integration/integration.cabal b/integration/integration.cabal index 0cdf6972ef1..03df02b4383 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -141,6 +141,7 @@ library Test.FeatureFlags.Apps Test.FeatureFlags.AssetAuditLog Test.FeatureFlags.Cells + Test.FeatureFlags.CellsInternal Test.FeatureFlags.Channels Test.FeatureFlags.ChatBubbles Test.FeatureFlags.ClassifiedDomains diff --git a/integration/test/Test/FeatureFlags/Cells.hs b/integration/test/Test/FeatureFlags/Cells.hs index fdb14b7c274..98fab115b76 100644 --- a/integration/test/Test/FeatureFlags/Cells.hs +++ b/integration/test/Test/FeatureFlags/Cells.hs @@ -17,18 +17,16 @@ module Test.FeatureFlags.Cells where -import qualified API.GalleyInternal as Internal -import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude +testCells :: (HasCallStack) => APIAccess -> App () +testCells access = + mkFeatureTests "cells" + & addUpdate enabled + & addUpdate disabled + & addInvalidUpdate (object []) + & runFeatureTests OwnDomain access + testPatchCells :: (HasCallStack) => App () testPatchCells = checkPatch OwnDomain "cells" enabled - -testCellsInternal :: (HasCallStack) => App () -testCellsInternal = do - (alice, tid, _) <- createTeam OwnDomain 0 - Internal.setTeamFeatureLockStatus alice tid "cells" "unlocked" - withWebSocket alice $ \ws -> do - setFlag InternalAPI ws tid "cells" enabled - setFlag InternalAPI ws tid "cells" disabled diff --git a/integration/test/Test/FeatureFlags/CellsInternal.hs b/integration/test/Test/FeatureFlags/CellsInternal.hs new file mode 100644 index 00000000000..550d079d140 --- /dev/null +++ b/integration/test/Test/FeatureFlags/CellsInternal.hs @@ -0,0 +1,113 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 Test.FeatureFlags.CellsInternal where + +import qualified API.GalleyInternal as Internal +import SetupHelpers +import Test.Cells (QueueConsumer (..), getMessage, watchCellsEvents) +import Test.FeatureFlags.Util +import Testlib.Prelude + +testCellsInternalEvent :: (HasCallStack) => App () +testCellsInternalEvent = do + (alice, tid, _) <- createTeam OwnDomain 0 + q <- do + q <- watchCellsEvents def + let isEventForTeam v = fieldEquals @Value v "payload.0.team" tid + -- the cells event queue is shared by tests + -- let's hope this filter reduces the risk of tests interfering with each other + pure $ q {filter = isEventForTeam} + let quota = "234723984" + update = mkFt "enabled" "unlocked" defConf {quota} + setFeature InternalAPI alice tid "cellsInternal" update >>= assertSuccess + event <- getMessage q %. "payload.0" + event %. "name" `shouldMatch` "cellsInternal" + event %. "team" `shouldMatch` tid + event %. "type" `shouldMatch` "feature-config.update" + event %. "data.lockStatus" `shouldMatch` "unlocked" + event %. "data.status" `shouldMatch` "enabled" + event %. "data.config.backend.url" `shouldMatch` "https://cells-beta.wire.com" + event %. "data.config.collabora.edition" `shouldMatch` "COOL" + event %. "data.config.storage.teamQuotaBytes" `shouldMatch` quota + +testCellsInternal :: (HasCallStack) => App () +testCellsInternal = do + (alice, tid, _) <- createTeam OwnDomain 0 + + withWebSocket alice $ \ws -> do + for_ validCellsInternalUpdates $ setFlag InternalAPI ws tid "cellsInternal" + for_ invalidCellsInternalUpdates $ setFeature InternalAPI alice tid "cellsInternal" >=> getJSON 400 + + -- the feature does not have a public PUT endpoint + setFeature PublicAPI alice tid "cellsInternal" enabled `bindResponse` \resp -> do + resp.status `shouldMatchInt` 404 + resp.json %. "label" `shouldMatch` "no-endpoint" + +validCellsInternalUpdates :: [Value] +validCellsInternalUpdates = + [ mkFt "enabled" "unlocked" defConf, + mkFt "enabled" "unlocked" defConf {collabora = "NO"}, + mkFt "enabled" "unlocked" defConf {collabora = "COOL"}, + mkFt "enabled" "unlocked" defConf {url = "https://wire.com"}, + mkFt "enabled" "unlocked" defConf {quota = "92346832946243"} + ] + +invalidCellsInternalUpdates :: [Value] +invalidCellsInternalUpdates = + [ mkFt "enabled" "unlocked" defConf {collabora = "FOO"}, + mkFt "enabled" "unlocked" defConf {url = "http://wire.com"}, + mkFt "enabled" "unlocked" defConf {quota = "-92346832946243"}, + mkFt "enabled" "unlocked" defConf {quota = "1 TB"}, + mkFt "disabled" "unlocked" defConf + ] + +mkFt :: String -> String -> CellsInternalConfig -> Value +mkFt s ls c = + object + [ "lockStatus" .= ls, + "status" .= s, + "ttl" .= "unlimited", + "config" + .= object + [ "backend" .= object ["url" .= c.url], + "collabora" .= object ["edition" .= c.collabora], + "storage" .= object ["teamQuotaBytes" .= c.quota] + ] + ] + +defConf :: CellsInternalConfig +defConf = + CellsInternalConfig + { url = "https://cells-beta.wire.com", + collabora = "COOL", + quota = "1000000000000" + } + +testPatchCellsInternal :: (HasCallStack) => App () +testPatchCellsInternal = do + for_ validCellsInternalUpdates $ checkPatch OwnDomain "cellsInternal" + (_, tid, _) <- createTeam OwnDomain 0 + for_ (mkFt "enabled" "locked" defConf : invalidCellsInternalUpdates) + $ Internal.patchTeamFeature OwnDomain tid "cellsInternal" + >=> assertStatus 400 + +data CellsInternalConfig = CellsInternalConfig + { url :: String, + collabora :: String, + quota :: String + } diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index d1d646d8099..7ab2eebc654 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -165,7 +165,19 @@ defAllFeatures = "chatBubbles" .= disabledLocked, "apps" .= disabledLocked, "simplifiedUserConnectionRequestQRCode" .= enabled, - "stealthUsers" .= disabledLocked + "stealthUsers" .= disabledLocked, + "cellsInternal" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "enabled", + "ttl" .= "unlimited", + "config" + .= object + [ "backend" .= object ["url" .= "https://cells-beta.wire.com"], + "collabora" .= object ["edition" .= "COOL"], + "storage" .= object ["teamQuotaBytes" .= "1000000000000"] + ] + ] ] hasExplicitLockStatus :: String -> Bool diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index eb920a261af..d63df1d3f79 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -217,6 +217,13 @@ newtype instance FeatureDefaults ChannelsConfig deriving (FromJSON) via Defaults (LockableFeature ChannelsConfig) deriving (ParseFeatureDefaults) via OptionalField ChannelsConfig +newtype instance FeatureDefaults CellsInternalConfig + = CellsInternalDefaults (LockableFeature CellsInternalConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature CellsInternalConfig) + deriving (ParseFeatureDefaults) via OptionalField CellsInternalConfig + data instance FeatureDefaults ExposeInvitationURLsToTeamAdminConfig = ExposeInvitationURLsToTeamAdminDefaults deriving stock (Eq, Show) diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index d9899c158b5..540f6391cbb 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -428,6 +428,7 @@ data TeamFeatureError | MLSProtocolMismatch | MLSE2EIDMissingCrlProxy | EmptyDownloadLocation + | InvalidStatusUpdate instance IsSwaggerError TeamFeatureError where -- Do not display in Swagger @@ -460,6 +461,7 @@ instance (Member (Error DynError) r) => ServerEffect (Error TeamFeatureError) r MLSProtocolMismatch -> DynError 400 "mls-protocol-mismatch" "The default protocol needs to be part of the supported protocols" MLSE2EIDMissingCrlProxy -> DynError 400 "mls-e2eid-missing-crl-proxy" "The field 'crlProxy' is missing in the request payload" EmptyDownloadLocation -> DynError 400 "empty-download-location" "Download location cannot be empty" + InvalidStatusUpdate -> DynError 400 "invalid-status-update" "Status must be enabled and lock status must be unlocked" type instance ErrorEffect TeamFeatureError = Error TeamFeatureError diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index d7f5bdf35a5..d08b09ce336 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -74,6 +74,7 @@ type FeatureAPI = :<|> FeatureAPIGet AppsConfig :<|> FeatureAPIGet SimplifiedUserConnectionRequestQRCodeConfig :<|> FeatureAPIGet StealthUsersConfig + :<|> FeatureAPIGet CellsInternalConfig type DeprecationNotice1 = "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 01727938772..470a382258a 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -86,6 +86,13 @@ module Wire.API.Team.Feature LimitedEventFanoutConfig (..), DomainRegistrationConfig (..), CellsConfig (..), + CellsInternalConfig, + CellsInternalConfigB (..), + CellsCollabora (..), + CollaboraEdition (..), + CellsBackend (..), + CellsStorage (..), + NumBytes (..), AllowedGlobalOperationsConfig (..), AssetAuditLogConfig (..), ConsumableNotificationsConfig (..), @@ -126,7 +133,7 @@ import Data.Id import Data.Json.Util import Data.Kind import Data.Map qualified as M -import Data.Misc (HttpsUrl) +import Data.Misc (HttpsUrl (..)) import Data.Monoid hiding (All, First) import Data.OpenApi qualified as S import Data.Proxy @@ -139,15 +146,17 @@ import Data.Text.Encoding qualified as T import Data.Text.Encoding.Error import Data.Text.Lazy qualified as TL import Data.Text.Lazy.Encoding qualified as TL +import Data.Text.Read qualified as TR import Data.Time import Deriving.Aeson import GHC.TypeLits import Generics.SOP qualified as GSOP import Imports hiding (All, First) import Servant (FromHttpApiData (..), ToHttpApiData (..)) -import Test.QuickCheck (getPrintableString) +import Test.QuickCheck (choose, getPrintableString) import Test.QuickCheck.Arbitrary (arbitrary) import Test.QuickCheck.Gen (suchThat) +import URI.ByteString.QQ qualified as URI.QQ import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.Routes.Named hiding (unnamed) @@ -256,6 +265,7 @@ data FeatureSingleton cfg where FeatureSingletonSimplifiedUserConnectionRequestQRCodeConfig :: FeatureSingleton SimplifiedUserConnectionRequestQRCodeConfig FeatureSingletonAssetAuditLogConfig :: FeatureSingleton AssetAuditLogConfig FeatureSingletonStealthUsersConfig :: FeatureSingleton StealthUsersConfig + FeatureSingletonCellsInternalConfig :: FeatureSingleton CellsInternalConfig type family DeprecatedFeatureName (v :: Version) (cfg :: Type) :: Symbol @@ -1432,7 +1442,6 @@ instance IsFeatureConfig DomainRegistrationConfig where -------------------------------------------------------------------------------- -- Cells feature --- | This feature does not have a PUT endpoint. See Note [unsettable features]. data CellsConfig = CellsConfig deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform CellsConfig) @@ -1450,6 +1459,137 @@ instance IsFeatureConfig CellsConfig where featureSingleton = FeatureSingletonCellsConfig objectSchema = pure CellsConfig +---------------------------------------------------------------------- +-- Cells Internal + +data CollaboraEdition = No | Code | Cool + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CollaboraEdition + deriving (Arbitrary) via (GenericUniform CollaboraEdition) + +instance ToSchema CollaboraEdition where + schema = + enum @Text "CollaboraEdition" $ + mconcat + [ element "NO" No, + element "CODE" Code, + element "COOL" Cool + ] + +newtype CellsCollabora = CellsCollabora + { edition :: CollaboraEdition + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsCollabora + deriving (Arbitrary) via (GenericUniform CellsCollabora) + +instance ToSchema CellsCollabora where + schema = + object "CellsCollabora" $ + CellsCollabora + <$> edition .= field "edition" schema + +newtype CellsBackend = CellsBackend + { url :: HttpsUrl + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsBackend + deriving newtype (Arbitrary) + +instance ToSchema CellsBackend where + schema = object "CellsBackend" $ CellsBackend <$> url .= field "url" schema + +newtype NumBytes = NumBytes {unNumBytes :: Int64} + deriving newtype (Show, Eq) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema NumBytes + +instance Arbitrary NumBytes where + arbitrary = NumBytes <$> choose (0 :: Int64, maxBound) + +instance ToSchema NumBytes where + schema = toText .= (NumBytes <$> numBytesSchema) + where + toText :: NumBytes -> Text + toText = T.pack . show . unNumBytes + + numBytesSchema :: ValueSchemaP NamedSwaggerDoc Text Int64 + numBytesSchema = schema `withParser` parseNumBytes + where + parseNumBytes :: Text -> A.Parser Int64 + parseNumBytes txt = do + (n, rest) <- either fail pure (TR.decimal txt :: Either String (Integer, Text)) + unless (T.null rest) $ + fail "numBytes must be an integer string without decimals" + when (n <= 0) $ + fail "numBytes must be positive" + when (n > toInteger (maxBound @Int64)) $ + fail "numBytes must fit into Int64" + pure (fromInteger n) + +newtype CellsStorage = CellsStorage + { teamQuotaBytes :: NumBytes + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsStorage + deriving newtype (Arbitrary) + +instance ToSchema CellsStorage where + schema = + object "CellsStorage" $ + CellsStorage + <$> teamQuotaBytes .= field "teamQuotaBytes" schema + +data CellsInternalConfigB t f = CellsInternalConfig + { backend :: Wear t f CellsBackend, + collabora :: Wear t f CellsCollabora, + storage :: Wear t f CellsStorage + } + deriving (Generic, BareB) + +deriving instance FunctorB (CellsInternalConfigB Covered) + +deriving instance ApplicativeB (CellsInternalConfigB Covered) + +deriving instance TraversableB (CellsInternalConfigB Covered) + +type CellsInternalConfig = CellsInternalConfigB Bare Identity + +deriving instance Eq CellsInternalConfig + +deriving instance Show CellsInternalConfig + +deriving via (RenderableTypeName CellsInternalConfig) instance (RenderableSymbol CellsInternalConfig) + +deriving via (GenericUniform CellsInternalConfig) instance (Arbitrary CellsInternalConfig) + +deriving via (BarbieFeature CellsInternalConfigB) instance (ParseDbFeature CellsInternalConfig) + +deriving via (BarbieFeature CellsInternalConfigB) instance (ToSchema CellsInternalConfig) + +instance Default CellsInternalConfig where + def = + CellsInternalConfig + { backend = CellsBackend $ HttpsUrl [URI.QQ.uri|https://cells-beta.wire.com|], + collabora = CellsCollabora Cool, + storage = CellsStorage $ NumBytes 1000000000000 -- 1 TB + } + +instance (FieldF f) => ToSchema (CellsInternalConfigB Covered f) where + schema = + object "CellsInternalConfig" $ + CellsInternalConfig + <$> backend .= fieldF "backend" schema + <*> collabora .= fieldF "collabora" schema + <*> storage .= fieldF "storage" schema + +instance Default (LockableFeature CellsInternalConfig) where + def = defUnlockedFeature + +instance IsFeatureConfig CellsInternalConfig where + type FeatureSymbol CellsInternalConfig = "cellsInternal" + featureSingleton = FeatureSingletonCellsInternalConfig + objectSchema = field "config" schema + -------------------------------------------------------------------------------- -- Allowed Global Operations feature @@ -1713,7 +1853,8 @@ type Features = AppsConfig, SimplifiedUserConnectionRequestQRCodeConfig, AssetAuditLogConfig, - StealthUsersConfig + StealthUsersConfig, + CellsInternalConfig ] -- | list of available features as a record @@ -1895,7 +2036,7 @@ mkAllFeatures m = isCellsFeatureConfigEvent :: forall cfg. (IsFeatureConfig cfg) => Bool isCellsFeatureConfigEvent = - featureName @cfg == featureName @CellsConfig + featureName @cfg `elem` [featureName @CellsConfig, featureName @CellsInternalConfig] -------------------------------------------------------------------------------- -- Team Feature Migration diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 5cc259464b6..30d103eded1 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -163,6 +163,17 @@ settings: defaults: status: enabled lockStatus: unlocked + cellsInternal: + defaults: + status: enabled + lockStatus: unlocked + config: + backend: + url: https://cells-beta.wire.com + collabora: + edition: COOL + storage: + teamQuotaBytes: "1000000000000" allowedGlobalOperations: status: enabled config: diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index ce8c980a59e..5e738062b26 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -291,6 +291,7 @@ allFeaturesAPI = <@> featureAPI1Full <@> featureAPI1Get <@> featureAPI1Full + <@> featureAPI1Full featureAPI :: API IFeatureAPI GalleyEffects featureAPI = diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 1313ac8ed80..9cb5266deda 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -75,6 +75,7 @@ featureAPI = <@> mkNamedAPI @'("get", AppsConfig) getFeature <@> mkNamedAPI @'("get", SimplifiedUserConnectionRequestQRCodeConfig) getFeature <@> mkNamedAPI @'("get", StealthUsersConfig) getFeature + <@> mkNamedAPI @'("get", CellsInternalConfig) getFeature deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects deprecatedFeatureConfigAPI = diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 0359adec7d2..72da3558e03 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -467,6 +467,15 @@ instance SetFeatureConfig DomainRegistrationConfig instance SetFeatureConfig CellsConfig +instance SetFeatureConfig CellsInternalConfig where + type + SetFeatureForTeamConstraints CellsInternalConfig r = + (Member (Error TeamFeatureError) r) + + prepareFeature _ feat = do + unless (feat.status == FeatureStatusEnabled && feat.lockStatus == LockStatusUnlocked) $ do + throw InvalidStatusUpdate + instance SetFeatureConfig ConsumableNotificationsConfig instance SetFeatureConfig ChatBubblesConfig diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index 12ef28d651b..ff817e01696 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -412,6 +412,8 @@ instance GetFeatureConfig DomainRegistrationConfig instance GetFeatureConfig CellsConfig +instance GetFeatureConfig CellsInternalConfig + instance GetFeatureConfig AllowedGlobalOperationsConfig instance GetFeatureConfig AssetAuditLogConfig diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 98fcb14c3cc..91a78d0ee48 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -174,6 +174,8 @@ sitemap' = :<|> Named @"put-route-enforce-file-download-location" (mkFeaturePutRoute @EnforceFileDownloadLocationConfig) :<|> Named @"get-route-cells" (mkFeatureGetRoute @CellsConfig) :<|> Named @"put-route-cells" (mkFeatureStatusPutRoute @CellsConfig) + :<|> Named @"get-route-cells-internal" (mkFeatureGetRoute @CellsInternalConfig) + :<|> Named @"put-route-cells-internal" (mkFeaturePutRoute @CellsInternalConfig) :<|> Named @"get-route-guest-links" (mkFeatureGetRoute @GuestLinksConfig) :<|> Named @"put-route-guest-links" (mkFeatureStatusPutRoute @GuestLinksConfig) :<|> Named @"get-route-self-deleting-messages" (mkFeatureGetRoute @SelfDeletingMessagesConfig) diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 086bcc6e061..4ca6a56932d 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -309,6 +309,8 @@ type SternAPI = ) :<|> Named "get-route-cells" (MkFeatureGetRoute CellsConfig) :<|> Named "put-route-cells" (MkFeatureStatusPutRoute CellsConfig) + :<|> Named "get-route-cells-internal" (MkFeatureGetRoute CellsInternalConfig) + :<|> Named "put-route-cells-internal" (MkFeaturePutRoute CellsInternalConfig) :<|> Named "get-route-guest-links" (MkFeatureGetRoute GuestLinksConfig) :<|> Named "put-route-guest-links" (MkFeatureStatusPutRoute GuestLinksConfig) :<|> Named "get-route-self-deleting-messages" (MkFeatureGetRoute SelfDeletingMessagesConfig) diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 19be283d2fb..e82e73711e9 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -33,6 +33,7 @@ import Data.ByteString.Conversion import Data.Default import Data.Handle import Data.Id +import Data.Misc (HttpsUrl) import Data.Range (unsafeRange) import Data.Schema import Data.Set qualified as Set @@ -116,6 +117,7 @@ tests s = test s "PUT /teams/:tid/features/sndFactorPasswordChallenge{,'?lockOrUnlock'}" $ testLockStatus @SndFactorPasswordChallengeConfig, test s "PUT /teams/:tid/features/limitedEventFanout{,'?lockOrUnlock'}" $ testLockStatus @LimitedEventFanoutConfig, test s "PUT /teams/:tid/features/cells{,'?lockOrUnlock'}" $ testLockStatus @CellsConfig, + test s "/teams/:tid/features/cellsInternal" testCellsInternalConfig, test s "PUT /teams/:tid/features/consumableNotifications{,'?lockOrUnlock'}" $ testLockStatus @ConsumableNotificationsConfig, test s "PUT /teams/:tid/features/chatBubbles{,'?lockOrUnlock'}" $ testLockStatus @ChatBubblesConfig, test s "/teams/:tid/features/chatBubbles" $ testFeatureStatus @ChatBubblesConfig, @@ -327,6 +329,27 @@ testFeatureConfig = do cfg' <- getFeatureConfig @cfg tid liftIO $ cfg'.status @?= newStatus +testCellsInternalConfig :: TestM () +testCellsInternalConfig = do + (_, tid, _) <- createTeamWithNMembers 1 + cfg <- getFeatureConfig @CellsInternalConfig tid + let newBackend :: HttpsUrl + newBackend = fromMaybe (error "invalid url") . fromByteString $ "https://cells-internal.example.com" + newCfg = + cfg + { config = + cfg.config + { backend = CellsBackend newBackend, + collabora = CellsCollabora Cool, + storage = CellsStorage (NumBytes 2000000000000) + } + } :: + LockableFeature CellsInternalConfig + + putFeatureConfig @CellsInternalConfig tid newCfg !!! const 200 === statusCode + cfg' <- getFeatureConfig @CellsInternalConfig tid + liftIO $ cfg' @?= newCfg + testGetFeatureConfig :: forall cfg. ( KnownSymbol (FeatureSymbol cfg), From 2b960a6695f3a251395475a9d42438ae4c631527 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 8 Dec 2025 10:01:14 +0100 Subject: [PATCH 09/60] Polish ScimSubsystem errors. (#4862) --- .../src/Wire/ScimSubsystem/Error.hs | 47 +++++++++++++++++++ .../src/Wire/ScimSubsystem/Interpreter.hs | 43 +++++++---------- libs/wire-subsystems/wire-subsystems.cabal | 1 + services/spar/src/Spar/Error.hs | 19 +------- 4 files changed, 66 insertions(+), 44 deletions(-) create mode 100644 libs/wire-subsystems/src/Wire/ScimSubsystem/Error.hs diff --git a/libs/wire-subsystems/src/Wire/ScimSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/ScimSubsystem/Error.hs new file mode 100644 index 00000000000..1be5ccd0852 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ScimSubsystem/Error.hs @@ -0,0 +1,47 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.ScimSubsystem.Error where + +import Data.Aeson +import Data.ByteString.Lazy as LBS +import Data.Id +import Data.Text as T +import Data.Text.Encoding as T +import Imports +import Network.Wai.Utilities.Error qualified as Wai +import Web.Scim.Schema.Error + +data ScimSubsystemError + = ScimSubsystemBadGroupName Text + | ScimSubsystemGroupNotFound UserGroupId + | ScimSubsystemUserNotFound UserId + | ScimSubsystemInvalidGroupMemberId Text + | ScimSubsystemGroupMembersNotFound [UserId] + | ScimSubsystemForbidden UserGroupId + | ScimSubsystemInternal Wai.Error + deriving (Show, Eq) + +scimSubsystemErrorToScimError :: ScimSubsystemError -> ScimError +scimSubsystemErrorToScimError = \case + ScimSubsystemBadGroupName bad -> badRequest InvalidValue (Just bad) + ScimSubsystemGroupNotFound bad -> notFound "Group" (idToText bad) + ScimSubsystemUserNotFound bad -> notFound "User" (idToText bad) + ScimSubsystemInvalidGroupMemberId bad -> badRequest InvalidValue (Just $ "Invalid group member ID: " <> bad) + ScimSubsystemGroupMembersNotFound bads -> badRequest InvalidValue (Just $ "These users do not exist, are not in your team, or not \"managed_by\" = \"scim\": " <> T.intercalate ", " (idToText <$> bads)) + ScimSubsystemForbidden bad -> forbidden ("Group is not managed by SCIM: " <> idToText bad) + ScimSubsystemInternal waiErr -> serverError (T.decodeUtf8 $ LBS.toStrict $ encode waiErr) diff --git a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs index 96132f3f6a6..fcac6f81468 100644 --- a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs @@ -15,26 +15,28 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.ScimSubsystem.Interpreter where +module Wire.ScimSubsystem.Interpreter + ( module Wire.ScimSubsystem.Error, + module Wire.ScimSubsystem.Interpreter, + ) +where import Data.Default import Data.Id import Data.Json.Util import Data.Set qualified as Set import Data.Text qualified as Text -import Data.UUID qualified as UUID import Data.Vector qualified as V import Imports import Network.HTTP.Types.Status (notFound404) import Network.URI (parseURI) -import Network.Wai.Utilities.Error qualified as Wai +import Network.Wai.Utilities.Error qualified as Error import Polysemy import Polysemy.Error import Polysemy.Input import Web.Scim.Class.Group qualified as SCG import Web.Scim.Filter qualified as Scim import Web.Scim.Schema.Common qualified as Common -import Web.Scim.Schema.Error import Web.Scim.Schema.ListResponse qualified as Scim import Web.Scim.Schema.Meta qualified as Meta import Web.Scim.Schema.ResourceType qualified as RT @@ -46,7 +48,7 @@ import Wire.API.UserGroup.Pagination import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.BrigAPIAccess qualified as BrigAPI import Wire.ScimSubsystem -import Wire.UserGroupSubsystem.Interpreter (UserGroupSubsystemError (..)) +import Wire.ScimSubsystem.Error data ScimSubsystemConfig = ScimSubsystemConfig { scimBaseUri :: Common.URI @@ -65,17 +67,6 @@ interpretScimSubsystem = interpret $ \case ScimUpdateUserGroup teamId userGroupId scimGroup -> scimUpdateUserGroupImpl teamId userGroupId scimGroup ScimDeleteUserGroup teamId groupId -> deleteScimGroupImpl teamId groupId -data ScimSubsystemError - = ScimSubsystemError ScimError -- TODO: replace this with custom constructors. (also, where are ScimSubsystemInvalidGroupMemberId etc. translated back into ScimErrors?) - | ScimSubsystemInvalidGroupMemberId Text - | ScimSubsystemGroupMembersNotFound [UserId] - | ScimSubsystemInternal Wai.Error - | ScimSubsystemInternalError UserGroupSubsystemError - deriving (Show, Eq) - -scimThrow :: (Member (Error ScimSubsystemError) r) => ScimError -> Sem r a -scimThrow = throw . ScimSubsystemError - createScimGroupImpl :: forall r. ( Member (Input ScimSubsystemConfig) r, @@ -98,12 +89,12 @@ createScimGroupImpl teamId grp = do ugName <- userGroupNameFromText grp.displayName - & either (scimThrow . badRequest InvalidValue . Just) pure + & either (throw . ScimSubsystemBadGroupName) pure ugMemberIds <- let go :: SCG.Member -> Sem r UserId go m = parseIdFromText m.value - & either (scimThrow . badRequest InvalidValue . Just . Text.pack) pure + & either (throw . ScimSubsystemInvalidGroupMemberId . Text.pack) pure in go `mapM` grp.members let newGroup = NewUserGroup {name = ugName, members = V.fromList ugMemberIds} @@ -127,7 +118,7 @@ scimGetUserGroupImpl tid gid = do let includeChannels = False -- SCIM has no notion of channels. maybe groupNotFound returnStoredGroup =<< BrigAPI.getGroupInternal tid gid includeChannels where - groupNotFound = scimThrow $ notFound "Group" $ UUID.toText $ toUUID gid + groupNotFound = throw (ScimSubsystemGroupNotFound gid) returnStoredGroup g = do ScimSubsystemConfig scimBaseUri <- input pure $ toStoredGroup scimBaseUri g @@ -159,11 +150,11 @@ scimUpdateUserGroupImpl teamId gid grp = do let includeChannels = False ug <- BrigAPI.getGroupInternal teamId gid includeChannels - >>= note (ScimSubsystemError $ notFound "Group" (UUID.toText $ gid.toUUID)) + >>= note (ScimSubsystemGroupNotFound gid) when (ug.managedBy /= ManagedByScim) do - scimThrow $ notFound "Group" (UUID.toText $ gid.toUUID) + throw (ScimSubsystemGroupNotFound gid) - ugName <- either (scimThrow . badRequest InvalidValue . Just) pure $ userGroupNameFromText grp.displayName + ugName <- either (throw . ScimSubsystemBadGroupName) pure $ userGroupNameFromText grp.displayName reqMemberIds <- for grp.members parseMember let currentSet = Set.fromList (toList (runIdentity ug.members)) @@ -179,18 +170,18 @@ scimUpdateUserGroupImpl teamId gid grp = do throw (ScimSubsystemGroupMembersNotFound notInTeamOrNotScim) case missing of [] -> pure () - (u : _) -> scimThrow $ notFound "User" (idToText u) + (u : _) -> throw (ScimSubsystemUserNotFound u) -- replace the members of the user group; propagate Brig errors BrigAPI.updateGroup (UpdateGroupInternalRequest teamId gid (Just ugName) (Just reqMemberIds)) >>= \case Right () -> pure () Left err -> if err.code == notFound404 - then scimThrow $ notFound "Group" (UUID.toText $ gid.toUUID) + then throw (ScimSubsystemGroupNotFound gid) else throw (ScimSubsystemInternal err) ScimSubsystemConfig scimBaseUri <- input - maybe (scimThrow $ notFound "Group" (UUID.toText $ gid.toUUID)) (pure . toStoredGroup scimBaseUri) + maybe (throw $ ScimSubsystemGroupNotFound gid) (pure . toStoredGroup scimBaseUri) =<< BrigAPI.getGroupInternal teamId gid includeChannels deleteScimGroupImpl :: @@ -205,7 +196,7 @@ deleteScimGroupImpl teamId groupId = do eResult <- BrigAPI.deleteGroupInternal ManagedByScim teamId groupId case eResult of Right () -> pure () - Left BrigAPI.DeleteGroupManagedManagedByMismatch -> scimThrow (forbidden "Cannot delete group not managed by SCIM") + Left BrigAPI.DeleteGroupManagedManagedByMismatch -> throw (ScimSubsystemForbidden groupId) toStoredGroup :: Common.URI -> UserGroup -> SCG.StoredGroup SparTag toStoredGroup scimBaseUri ug = Meta.WithMeta meta (Common.WithId ug.id_ sg) diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 2d4cef48c64..f8ea7fdff63 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -292,6 +292,7 @@ library Wire.RateLimit.Interpreter Wire.Rpc Wire.ScimSubsystem + Wire.ScimSubsystem.Error Wire.ScimSubsystem.Interpreter Wire.ServiceStore Wire.ServiceStore.Cassandra diff --git a/services/spar/src/Spar/Error.hs b/services/spar/src/Spar/Error.hs index 526b4ce8713..48ff8866dcc 100644 --- a/services/spar/src/Spar/Error.hs +++ b/services/spar/src/Spar/Error.hs @@ -48,9 +48,6 @@ import qualified Bilge import Control.Monad.Except import Data.Aeson import qualified Data.ByteString.Lazy as LBS -import Data.Id -import qualified Data.Text as Text -import qualified Data.Text.Encoding as Text import qualified Data.Text.Lazy as LText import qualified Data.Text.Lazy.Encoding as LText import Data.Typeable (typeRep) @@ -291,18 +288,4 @@ parseResponse serviceName resp = do mapScimSubsystemErrors :: (Member (Error SparError) r) => InterpreterFor (Error ScimSubsystemError) r mapScimSubsystemErrors = - Polysemy.Error.mapError $ - SAML.CustomError . SparScimError . \case - ScimSubsystemError err -> - err - ScimSubsystemInvalidGroupMemberId badId -> - Scim.badRequest Scim.InvalidValue (Just $ "Invalid group member ID: " <> badId) - ScimSubsystemGroupMembersNotFound badIds -> - Scim.badRequest Scim.InvalidValue (Just $ "These users are not in your team or not \"managed_by\" = \"scim\": " <> renderIds badIds) - ScimSubsystemInternal waiErr -> - Scim.serverError (Text.decodeUtf8 . LBS.toStrict $ encode waiErr) - ScimSubsystemInternalError _ -> - Scim.serverError "unexpected error" - where - renderIds :: [UserId] -> Text - renderIds = Text.intercalate ", " . fmap idToText + Polysemy.Error.mapError (SAML.CustomError . SparScimError . scimSubsystemErrorToScimError) From 4a5089264c1a918378a494357346fbad2da45301 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 8 Dec 2025 13:36:30 +0100 Subject: [PATCH 10/60] ConversationStore.Migration: log and emit metric if a migration fails (#4891) --- .../2-features/log-errors-in-conv-migration | 7 ++++ .../src/Wire/ConversationStore/Migration.hs | 33 ++++++++++++------- .../src/Wire/MigrateConversations.hs | 10 +++--- 3 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 changelog.d/2-features/log-errors-in-conv-migration diff --git a/changelog.d/2-features/log-errors-in-conv-migration b/changelog.d/2-features/log-errors-in-conv-migration new file mode 100644 index 00000000000..aba4edb77c9 --- /dev/null +++ b/changelog.d/2-features/log-errors-in-conv-migration @@ -0,0 +1,7 @@ +Introduce new metrics for better tracking of conversation migration to postgresql: +1. `wire_local_convs_migration_failed` +2. `wire_user_remote_convs_migration_failed` + +If any of these become `1`, it means the migration has failed. The logs would +contain the error. In order to restart the migration, the background-worker must +be restarted. \ No newline at end of file diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs index 2a968a106c1..295910399f6 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs @@ -52,6 +52,7 @@ import Polysemy.Time import Polysemy.TinyLog import Prometheus qualified import System.Logger qualified as Log +import UnliftIO.Exception qualified as UnliftIO import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.CellsState import Wire.API.Conversation.Protocol @@ -80,26 +81,36 @@ import Wire.Util type EffectStack = [State Int, Input ClientState, Input Hasql.Pool, Async, Race, TinyLog, Embed IO, Final IO] -migrateConvsLoop :: ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> IO () -migrateConvsLoop cassClient pgPool logger migCounter migFinished = - migrationLoop cassClient pgPool logger "conversations" migFinished $ migrateAllConversations migCounter +migrateConvsLoop :: ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO () +migrateConvsLoop cassClient pgPool logger migCounter migFinished migFailed = + migrationLoop cassClient pgPool logger "conversations" migFinished migFailed $ migrateAllConversations migCounter -migrateUsersLoop :: ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> IO () -migrateUsersLoop cassClient pgPool logger migCounter migFinished = - migrationLoop cassClient pgPool logger "users" migFinished $ migrateAllUsers migCounter +migrateUsersLoop :: ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO () +migrateUsersLoop cassClient pgPool logger migCounter migFinished migFailed = + migrationLoop cassClient pgPool logger "users" migFinished migFailed $ migrateAllUsers migCounter -migrationLoop :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Prometheus.Counter -> ConduitT () Void (Sem EffectStack) () -> IO () -migrationLoop cassClient pgPool logger name migFinished migration = do - go 0 - Prometheus.incCounter migFinished +migrationLoop :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Prometheus.Counter -> Prometheus.Counter -> ConduitT () Void (Sem EffectStack) () -> IO () +migrationLoop cassClient pgPool logger name migFinished migFailed migration = do + go 0 `UnliftIO.catch` handleIOError where + handleIOError :: SomeException -> IO () + handleIOError exc = do + Prometheus.incCounter migFailed + Log.err logger $ + Log.msg (Log.val "migration failed, it won't restart unless the background-worker is restarted.") + . Log.field "migration" name + . Log.field "error" (displayException exc) + UnliftIO.throwIO exc + go :: Int -> IO () go nIter = do runMigration >>= \case - 0 -> + 0 -> do Log.info logger $ Log.msg (Log.val "finished migration") . Log.field "attempt" nIter + . Log.field "migration" name + Prometheus.incCounter migFinished n -> do Log.info logger $ Log.msg (Log.val "finished migration with errors") diff --git a/services/background-worker/src/Wire/MigrateConversations.hs b/services/background-worker/src/Wire/MigrateConversations.hs index c2e4f318188..9e6503899e2 100644 --- a/services/background-worker/src/Wire/MigrateConversations.hs +++ b/services/background-worker/src/Wire/MigrateConversations.hs @@ -33,12 +33,14 @@ startWorker = do Log.info logger $ Log.msg (Log.val "starting conversation migration") convMigCounter <- register $ counter $ Prometheus.Info "wire_local_convs_migrated_to_pg" "Number of local conversations migrated to Postgresql" - convMigFinished <- register $ counter $ Prometheus.Info "wire_local_convs_migration_finished" "Whether the conversation migration to Postgresql is finished" + convMigFinished <- register $ counter $ Prometheus.Info "wire_local_convs_migration_finished" "Whether the conversation migration to Postgresql is finished successfully" + convMigFailed <- register $ counter $ Prometheus.Info "wire_local_convs_migration_failed" "Whether the conversation migration to Postgresql has failed" userMigCounter <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migrated_to_pg" "Number of users whose remote conversation membership data is migrated to Postgresql" - userMigFinished <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migration_finished" "Whether the migration of remote conversation membership data to Postgresql is finished" + userMigFinished <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migration_finished" "Whether the migration of remote conversation membership data to Postgresql is finished successfully" + userMigFailed <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migration_failed" "Whether the migration of remote conversation membership data to Postgresql has failed" - convLoop <- async . lift $ migrateConvsLoop cassClient pgPool logger convMigCounter convMigFinished - userLoop <- async . lift $ migrateUsersLoop cassClient pgPool logger userMigCounter userMigFinished + convLoop <- async . lift $ migrateConvsLoop cassClient pgPool logger convMigCounter convMigFinished convMigFailed + userLoop <- async . lift $ migrateUsersLoop cassClient pgPool logger userMigCounter userMigFinished userMigFailed Log.info logger $ Log.msg (Log.val "started conversation migration") pure $ do From de185ec3d560fc242c8aa3aace8eb7b48184f5b5 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 8 Dec 2025 15:36:05 +0100 Subject: [PATCH 11/60] Fix compiler error. (#4890) --- services/galley/src/Galley/API/Util.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index a89dfde31a4..aa6a22995e4 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -303,8 +303,7 @@ ensureConvRoleNotElevated origMember targetRole = do checkGroupIdSupport :: ( Member (ErrorS GroupIdVersionNotSupported) r, - Member (FederationAPIAccess FederatorClient) r, - VersionedMonad Version (FederatorClient Brig) + Member (FederationAPIAccess FederatorClient) r ) => Local x -> StoredConversation -> From ac2da9cc15d420f8703b642bb12c79200cc646b5 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 8 Dec 2025 16:01:24 +0100 Subject: [PATCH 12/60] Galley.API.Util: Remove redundant constraint (#4892) From e395db73af2daab2a026ee411dd088ac5a07cdc4 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 8 Dec 2025 17:19:33 +0100 Subject: [PATCH 13/60] Galley.API.Action.Reset: Remove redundant constraint (#4894) --- services/galley/src/Galley/API/Action/Reset.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/services/galley/src/Galley/API/Action/Reset.hs b/services/galley/src/Galley/API/Action/Reset.hs index 296959f8ee1..b483f293044 100644 --- a/services/galley/src/Galley/API/Action/Reset.hs +++ b/services/galley/src/Galley/API/Action/Reset.hs @@ -70,7 +70,6 @@ resetLocalMLSMainConversation :: Member ConversationStore r, Member P.TinyLog r, Member MLSCommitLockStore r, - VersionedMonad Version (FederatorClient Brig), Member (Input ConversationSubsystemConfig) r ) => Qualified UserId -> From 9e83d2de6e78b4d00d88fe23186de26bdda140ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20L=C3=A4ll?= Date: Tue, 9 Dec 2025 12:47:52 +0200 Subject: [PATCH 14/60] WPB-21294: Add fields to apps: category, description, creator; WPB-21295: Add "get app" endpoint (#4879) * Add app fields: category, description, creator * Add get app endpoint (stub; useful for integration tests) * Extend app API test * Add changelog Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ...dd-app-fields-category-description-creator | 1 + .../1-api-changes/add-get-app-endpoint | 1 + integration/test/API/Brig.hs | 29 ++++-- integration/test/Test/Apps.hs | 49 ++++++++-- libs/wire-api/src/Wire/API/App.hs | 98 ++++++++++++++++--- .../src/Wire/API/Routes/Public/Brig.hs | 10 ++ .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 4 +- ...pp-fields-category-description-creator.sql | 4 + libs/wire-subsystems/src/Wire/AppStore.hs | 26 +++-- .../src/Wire/AppStore/Postgres.hs | 15 +-- libs/wire-subsystems/src/Wire/AppSubsystem.hs | 12 ++- .../src/Wire/AppSubsystem/Interpreter.hs | 65 +++++++++--- .../src/Wire/UserSubsystem/Interpreter.hs | 4 +- .../unit/Wire/MockInterpreters/AppStore.hs | 2 +- postgres-schema.sql | 5 +- services/brig/src/Brig/API/Public.hs | 4 + 16 files changed, 268 insertions(+), 61 deletions(-) create mode 100644 changelog.d/1-api-changes/add-app-fields-category-description-creator create mode 100644 changelog.d/1-api-changes/add-get-app-endpoint create mode 100644 libs/wire-subsystems/postgres-migrations/20251127115917-app-fields-category-description-creator.sql diff --git a/changelog.d/1-api-changes/add-app-fields-category-description-creator b/changelog.d/1-api-changes/add-app-fields-category-description-creator new file mode 100644 index 00000000000..31cb223bed0 --- /dev/null +++ b/changelog.d/1-api-changes/add-app-fields-category-description-creator @@ -0,0 +1 @@ +Add new fields to apps: category, description, creator \ No newline at end of file diff --git a/changelog.d/1-api-changes/add-get-app-endpoint b/changelog.d/1-api-changes/add-get-app-endpoint new file mode 100644 index 00000000000..e433e9cc22e --- /dev/null +++ b/changelog.d/1-api-changes/add-get-app-endpoint @@ -0,0 +1 @@ +Add "get app" endpoint to Brig (`GET /teams/:tid/apps/:id`) \ No newline at end of file diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 1276c1c0585..5c7fee6fed2 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1204,7 +1204,9 @@ data NewApp = NewApp pict :: Maybe [Value], assets :: Maybe [Value], accentId :: Maybe Int, - meta :: Value + meta :: Value, + category :: String, + description :: String } instance Default NewApp where @@ -1214,7 +1216,9 @@ instance Default NewApp where pict = Nothing, assets = Nothing, accentId = Nothing, - meta = object [] + meta = object [], + category = "other", + description = "" } createApp :: (MakesValue creator) => creator -> String -> NewApp -> App Response @@ -1223,13 +1227,24 @@ createApp creator tid new = do submit "POST" $ req & addJSONObject - [ "name" .= new.name, - "picture" .= new.pict, - "assets" .= new.assets, - "accent_id" .= new.accentId, - "metadata" .= new.meta + [ "app" + .= object + [ "name" .= new.name, + "picture" .= new.pict, + "assets" .= new.assets, + "accent_id" .= new.accentId, + "metadata" .= new.meta, + "category" .= new.category, + "description" .= new.description + ], + "password" .= defPassword ] +getApp :: (MakesValue self) => self -> String -> String -> App Response +getApp self tid uid = do + req <- baseRequest self Brig Versioned $ joinHttpPath ["teams", tid, "apps", uid] + submit "GET" req + refreshAppCookie :: (MakesValue u) => u -> String -> String -> App Response refreshAppCookie u tid appId = do req <- baseRequest u Brig Versioned $ joinHttpPath ["teams", tid, "apps", appId, "cookies"] diff --git a/integration/test/Test/Apps.hs b/integration/test/Test/Apps.hs index 33637f21122..4c07bd24b82 100644 --- a/integration/test/Test/Apps.hs +++ b/integration/test/Test/Apps.hs @@ -26,27 +26,35 @@ import Testlib.Prelude testCreateApp :: (HasCallStack) => App () testCreateApp = do domain <- make OwnDomain - (alice, tid, [bob]) <- createTeam domain 2 - let new = def {name = "chappie"} :: NewApp - - bindResponse (createApp bob tid new) $ \resp -> do + (owner, tid, [regularMember]) <- createTeam domain 2 + let new = + def + { name = "chappie", + description = "some description of this app", + category = "ai" + } :: + NewApp + + -- Regular team member can't create apps + bindResponse (createApp regularMember tid new) $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "app-no-permission" - (appId, cookie) <- bindResponse (createApp alice tid new) $ \resp -> do + -- Owner can create an app + (appId, cookie) <- bindResponse (createApp owner tid new) $ \resp -> do resp.status `shouldMatchInt` 200 appId <- resp.json %. "user.id" & asString cookie <- resp.json %. "cookie" & asString pure (appId, cookie) - -- app user should have type "app" + -- App user should have type "app" let appIdObject = object ["domain" .= domain, "id" .= appId] - bindResponse (getUser alice appIdObject) $ \resp -> do + bindResponse (getUser owner appIdObject) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "type" `shouldMatch` "app" - -- creator should have type "regular" - bindResponse (getUser alice alice) $ \resp -> do + -- Creator should have type "regular" + bindResponse (getUser owner owner) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "type" `shouldMatch` "regular" @@ -56,6 +64,29 @@ testCreateApp = do resp.json %. "token_type" `shouldMatch` "Bearer" resp.json %. "access_token" & asString + -- Get app for the app created above succeeds + void $ getApp regularMember tid appId `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + (resp.json %. "name") `shouldMatch` "chappie" + (resp.json %. "description") `shouldMatch` "some description of this app" + (resp.json %. "category") `shouldMatch` "ai" + + -- A teamless user can't get the app + outsideUser <- randomUser OwnDomain def + bindResponse (getApp outsideUser tid appId) $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "app-no-permission" + + -- Another team's owner nor member can't get the app + (owner2, tid2, [regularMember2]) <- createTeam domain 2 + bindResponse (getApp owner2 tid appId) $ \resp -> resp.status `shouldMatchInt` 403 + bindResponse (getApp owner2 tid2 appId) $ \resp -> resp.status `shouldMatchInt` 404 + bindResponse (getApp regularMember2 tid appId) $ \resp -> resp.status `shouldMatchInt` 403 + + -- Category must be any of the values for the Category enum + void $ bindResponse (createApp owner tid new {category = "notinenum"}) $ \resp -> do + resp.status `shouldMatchInt` 400 + testRefreshAppCookie :: (HasCallStack) => App () testRefreshAppCookie = do (alice, tid, [bob]) <- createTeam OwnDomain 2 diff --git a/libs/wire-api/src/Wire/API/App.hs b/libs/wire-api/src/Wire/API/App.hs index 26f5492aca0..b93894bd0d5 100644 --- a/libs/wire-api/src/Wire/API/App.hs +++ b/libs/wire-api/src/Wire/API/App.hs @@ -18,40 +18,114 @@ module Wire.API.App where import Data.Aeson qualified as A +import Data.HashMap.Strict qualified as HM +import Data.Misc import Data.OpenApi qualified as S +import Data.Range import Data.Schema import Imports import Wire.API.User import Wire.API.User.Auth +import Wire.Arbitrary as Arbitrary data NewApp = NewApp + { app :: GetApp, + password :: PlainTextPassword6 + } + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema NewApp + +data GetApp = GetApp { name :: Name, pict :: Pict, assets :: [Asset], accentId :: ColourId, - meta :: A.Object + meta :: A.Object, + category :: Category, + description :: Range 0 300 Text } - deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema NewApp + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema GetApp + +data Category + = Security + | Collaboration + | Productivity + | Automation + | Files + | AI + | Developer + | Support + | Finance + | HR + | Integration + | Compliance + | Other + deriving (Eq, Ord, Show, Read, Generic) + deriving (Arbitrary) via GenericUniform Category + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Category) + +categoryTextMapping :: [(Text, Category)] +categoryTextMapping = + [ ("security", Security), + ("collaboration", Collaboration), + ("productivity", Productivity), + ("automation", Automation), + ("files", Files), + ("ai", AI), + ("developer", Developer), + ("support", Support), + ("finance", Finance), + ("hr", HR), + ("integration", Integration), + ("compliance", Compliance), + ("other", Other) + ] + +categoryMap :: HM.HashMap Text Category +categoryMap = HM.fromList categoryTextMapping + +categoryFromText :: Text -> Maybe Category +categoryFromText text' = HM.lookup text' categoryMap + +categoryToText :: Category -> Text +categoryToText = \case + Security -> "security" + Collaboration -> "collaboration" + Productivity -> "productivity" + Automation -> "automation" + Files -> "files" + AI -> "ai" + Developer -> "developer" + Support -> "support" + Finance -> "finance" + HR -> "hr" + Integration -> "integration" + Compliance -> "compliance" + Other -> "other" + +instance ToSchema Category where + schema = + enum @Text "Category" $ + mconcat $ + map (uncurry element) categoryTextMapping instance ToSchema NewApp where schema = object "NewApp" $ NewApp + <$> (.app) .= field "app" schema + <*> (.password) .= field "password" schema + +instance ToSchema GetApp where + schema = + object "GetApp" $ + GetApp <$> (.name) .= field "name" schema <*> (.pict) .= (fromMaybe noPict <$> optField "picture" schema) <*> (.assets) .= (fromMaybe [] <$> optField "assets" (array schema)) <*> (.accentId) .= (fromMaybe defaultAccentId <$> optField "accent_id" schema) <*> (.meta) .= field "metadata" jsonObject - -defNewApp :: Name -> NewApp -defNewApp name = - NewApp - { name, - pict = noPict, - assets = [], - accentId = defaultAccentId, - meta = mempty - } + <*> (.category) .= field "category" schema + <*> (.description) .= field "description" schema data CreatedApp = CreatedApp { user :: User, diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index ceac0a84454..f1207b00b13 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -2118,6 +2118,16 @@ type AppsAPI = :> ReqBody '[JSON] NewApp :> Post '[JSON] CreatedApp ) + :<|> Named + "get-app" + ( Summary "Get app" + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "apps" + :> Capture "id" UserId + :> Get '[JSON] GetApp + ) :<|> Named "refresh-app-cookie" ( Summary "Get a new app authentication token" 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 91f136c22bb..8d7c6e1d2b0 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 @@ -26,6 +26,7 @@ import Imports import Test.Tasty qualified as T import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (.&&.), (===)) import Type.Reflection (typeRep) +import Wire.API.App qualified as App import Wire.API.Asset qualified as Asset import Wire.API.BackgroundJobs qualified as BackgroundJobs import Wire.API.Call.Config qualified as Call.Config @@ -86,7 +87,8 @@ import Wire.API.Wrapped qualified as Wrapped tests :: T.TestTree tests = T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "JSON roundtrip tests" $ - [ testRoundTrip @Asset.AssetToken, + [ testRoundTrip @App.Category, + testRoundTrip @Asset.AssetToken, testRoundTrip @Asset.NewAssetToken, testRoundTrip @Asset.AssetRetention, testRoundTrip @Asset.AssetSettings, diff --git a/libs/wire-subsystems/postgres-migrations/20251127115917-app-fields-category-description-creator.sql b/libs/wire-subsystems/postgres-migrations/20251127115917-app-fields-category-description-creator.sql new file mode 100644 index 00000000000..ec8f60f154d --- /dev/null +++ b/libs/wire-subsystems/postgres-migrations/20251127115917-app-fields-category-description-creator.sql @@ -0,0 +1,4 @@ +ALTER TABLE apps + ADD COLUMN category text DEFAULT 'other' NOT NULL, + ADD COLUMN description text DEFAULT '' NOT NULL, + ADD COLUMN creator uuid NOT NULL; diff --git a/libs/wire-subsystems/src/Wire/AppStore.hs b/libs/wire-subsystems/src/Wire/AppStore.hs index e756a242f68..bc347da1662 100644 --- a/libs/wire-subsystems/src/Wire/AppStore.hs +++ b/libs/wire-subsystems/src/Wire/AppStore.hs @@ -21,34 +21,48 @@ module Wire.AppStore where import Data.Aeson import Data.Id +import Data.Range import Data.UUID import Imports import Polysemy +import Wire.API.App import Wire.API.PostgresMarshall data StoredApp = StoredApp { id :: UserId, teamId :: TeamId, - meta :: Object + meta :: Object, + category :: Category, + description :: Range 0 300 Text, + creator :: UserId } deriving (Eq, Ord, Show) -instance PostgresMarshall StoredApp (UUID, UUID, Value) where +-- The `PostgresMarshall` instances are here in this module -- as +-- having them elsewhere would make them orphan instances of +-- `StoredApp`. +instance PostgresMarshall StoredApp (UUID, UUID, Value, Text, Text, UUID) where postgresMarshall app = ( postgresMarshall app.id, postgresMarshall app.teamId, - postgresMarshall app.meta + postgresMarshall app.meta, + postgresMarshall (categoryToText app.category), + postgresMarshall (fromRange app.description), + postgresMarshall app.creator ) -instance PostgresUnmarshall (UUID, UUID, Value) StoredApp where - postgresUnmarshall (uid, teamId, meta) = +instance PostgresUnmarshall (UUID, UUID, Value, Text, Text, UUID) StoredApp where + postgresUnmarshall (uid, teamId, meta, category, description, creator) = StoredApp <$> postgresUnmarshall uid <*> postgresUnmarshall teamId <*> postgresUnmarshall meta + <*> (postgresUnmarshall =<< maybe (Left $ "Category " <> category <> " not found") Right (categoryFromText category)) + <*> (maybe (Left "description out of bounds") Right . checked @0 @300 =<< postgresUnmarshall description) + <*> postgresUnmarshall creator data AppStore m a where CreateApp :: StoredApp -> AppStore m () - GetApp :: UserId -> AppStore m (Maybe StoredApp) + GetApp :: UserId -> TeamId -> AppStore m (Maybe StoredApp) makeSem ''AppStore diff --git a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs index dc6b4c14dd3..d457003e3c9 100644 --- a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs @@ -42,7 +42,7 @@ interpretAppStoreToPostgres :: interpretAppStoreToPostgres = interpret $ \case CreateApp app -> createAppImpl app - GetApp userId -> getAppImpl userId + GetApp userId teamId -> getAppImpl userId teamId createAppImpl :: ( Member (Input Pool) r, @@ -55,8 +55,8 @@ createAppImpl app = runStatement app $ lmapPG [resultlessStatement| - insert into apps (user_id, team_id, metadata) - values ($1 :: uuid, $2 :: uuid, $3 :: json) |] + insert into apps (user_id, team_id, metadata, category, description, creator) + values ($1 :: uuid, $2 :: uuid, $3 :: json, $4 :: text, $5 :: text, $6 :: uuid) |] getAppImpl :: ( Member (Input Pool) r, @@ -64,9 +64,10 @@ getAppImpl :: Member (Error UsageError) r ) => UserId -> + TeamId -> Sem r (Maybe StoredApp) -getAppImpl uid = - runStatement uid $ +getAppImpl uid tid = + runStatement (uid, tid) $ dimapPG - [maybeStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json) - from apps where user_id = ($1 :: uuid) |] + [maybeStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json), (category :: text), (description :: text), (creator :: uuid) + from apps where user_id = ($1 :: uuid) and team_id = ($2 :: uuid) |] diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem.hs b/libs/wire-subsystems/src/Wire/AppSubsystem.hs index 38112be32ec..de5ae18a24a 100644 --- a/libs/wire-subsystems/src/Wire/AppSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/AppSubsystem.hs @@ -26,7 +26,7 @@ import Imports import Network.HTTP.Types.Status import Network.Wai.Utilities.Error qualified as Wai import Polysemy -import Wire.API.App +import Wire.API.App qualified as Apps import Wire.API.User import Wire.API.User.Auth import Wire.Error @@ -35,17 +35,23 @@ data AppSubsystemConfig = AppSubsystemConfig { defaultLocale :: Locale } -data AppSubsystemError = AppSubsystemErrorNoPerm | AppSubsystemErrorNoUser | AppSubsystemErrorNoApp +data AppSubsystemError + = AppSubsystemErrorNoPerm + | AppSubsystemErrorNoUser -- The user having created the app not found + | AppSubsystemErrorAppUserNotFound -- The user used to "enact" the app not found + | AppSubsystemErrorNoApp appSubsystemErrorToHttpError :: AppSubsystemError -> HttpError appSubsystemErrorToHttpError = StdError . \case AppSubsystemErrorNoPerm -> Wai.mkError status403 "app-no-permission" "User does not have permission to create or manage apps" AppSubsystemErrorNoUser -> Wai.mkError status403 "create-app-no-user" "App owner not found" + AppSubsystemErrorAppUserNotFound -> Wai.mkError status403 "app-user-not-found" "App user not found" AppSubsystemErrorNoApp -> Wai.mkError status404 "app-not-found" "App not found" data AppSubsystem m a where - CreateApp :: Local UserId -> TeamId -> NewApp -> AppSubsystem m CreatedApp + CreateApp :: Local UserId -> TeamId -> Apps.NewApp -> AppSubsystem m Apps.CreatedApp + GetApp :: Local UserId -> TeamId -> UserId -> AppSubsystem m Apps.GetApp RefreshAppCookie :: Local UserId -> TeamId -> diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs index 11d01611f33..1a901eb4204 100644 --- a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs @@ -31,7 +31,7 @@ import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import System.Logger.Message qualified as Log -import Wire.API.App +import Wire.API.App qualified as Apps import Wire.API.Event.Team import Wire.API.Team.Member qualified as T import Wire.API.User @@ -67,6 +67,7 @@ runAppSubsystem :: Sem r a runAppSubsystem = interpret \case CreateApp lusr tid new -> createAppImpl lusr tid new + GetApp lusr tid uid -> getAppImpl lusr tid uid RefreshAppCookie lusr tid appId -> runError $ refreshAppCookieImpl lusr tid appId createAppImpl :: @@ -84,12 +85,11 @@ createAppImpl :: ) => Local UserId -> TeamId -> - NewApp -> - Sem r CreatedApp -createAppImpl lusr tid new = do - creator <- Store.getUser (tUnqualified lusr) >>= note AppSubsystemErrorNoUser - - mem <- getTeamMember creator.id tid >>= note AppSubsystemErrorNoPerm + Apps.NewApp -> + Sem r Apps.CreatedApp +createAppImpl lusr tid (Apps.NewApp new password6) = do + verifyUserPasswordError lusr password6 + (creator, mem) <- ensureTeamMember lusr tid note AppSubsystemErrorNoPerm $ guard (T.hasPermission mem T.CreateApp) u <- appNewStoredUser creator new @@ -97,7 +97,10 @@ createAppImpl lusr tid new = do StoredApp { id = u.id, teamId = tid, - meta = new.meta + meta = new.meta, + category = new.category, + description = new.description, + creator = tUnqualified lusr } Log.info $ @@ -114,11 +117,50 @@ createAppImpl lusr tid new = do c :: Cookie (Token U) <- newCookie u.id Nothing PersistentCookie (Just "app") pure - CreatedApp + Apps.CreatedApp { user = newStoredUserToUser (tUntagged (qualifyAs lusr u)), cookie = mkSomeToken c.cookieValue } +-- | Check that @lusr@ is member of team with @tid@. +ensureTeamMember :: + ( Member UserStore r, + Member (Error AppSubsystemError) r, + Member GalleyAPIAccess r + ) => + Local UserId -> + TeamId -> + Sem r (StoredUser, T.TeamMember) +ensureTeamMember lusr tid = do + storedUser <- Store.getUser (tUnqualified lusr) >>= note AppSubsystemErrorNoUser + teamMember <- getTeamMember storedUser.id tid >>= note AppSubsystemErrorNoPerm + pure (storedUser, teamMember) + +getAppImpl :: + ( Member AppStore r, + Member (Error AppSubsystemError) r, + Member GalleyAPIAccess r, + Member UserStore r + ) => + Local UserId -> + TeamId -> + UserId -> + Sem r Apps.GetApp +getAppImpl lusr tid uid = do + void $ ensureTeamMember lusr tid + storedApp <- Store.getApp uid tid >>= note AppSubsystemErrorNoApp + u <- Store.getUser uid >>= note AppSubsystemErrorAppUserNotFound + pure $ + Apps.GetApp + { name = u.name, + pict = fromMaybe (Pict []) u.pict, + assets = fromMaybe [] u.assets, + accentId = u.accentId, + meta = storedApp.meta, + category = storedApp.category, + description = storedApp.description + } + refreshAppCookieImpl :: ( Member AuthenticationSubsystem r, Member AppStore r, @@ -133,8 +175,7 @@ refreshAppCookieImpl :: refreshAppCookieImpl (tUnqualified -> uid) tid appId = do mem <- getTeamMember uid tid >>= note AppSubsystemErrorNoPerm note AppSubsystemErrorNoPerm $ guard (T.hasPermission mem T.ManageApps) - app <- Store.getApp appId >>= note AppSubsystemErrorNoApp - note AppSubsystemErrorNoApp $ guard (app.teamId == tid) + void $ Store.getApp appId tid >>= note AppSubsystemErrorNoApp c :: Cookie (Token U) <- newCookieLimited appId Nothing PersistentCookie (Just "app") @@ -146,7 +187,7 @@ appNewStoredUser :: Member (Input AppSubsystemConfig) r ) => StoredUser -> - NewApp -> + Apps.GetApp -> Sem r NewStoredUser appNewStoredUser creator new = do uid <- liftIO nextRandom diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 6d5ec52c9ca..86be662b3c3 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -453,9 +453,9 @@ getLocalUserProfileImpl emailVisibilityConfigWithViewer luid = do pure $ maybe defUserLegalHoldStatus (view legalHoldStatus) teamMember let user = mkUserFromStored domain locale storedUser usrProfile = mkUserProfile emailVisibilityConfigWithViewer user lhs - app <- lift $ getApp storedUser.id + app <- lift $ mapM (getApp storedUser.id) storedUser.teamId lift $ deleteLocalIfExpired user - pure $ case app of + pure $ case join app of Nothing -> usrProfile Just _ -> usrProfile {profileType = UserTypeApp} diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs index 470a1dc1308..4eec779120b 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs @@ -30,4 +30,4 @@ inMemoryAppStoreInterpreter :: InterpreterFor AppStore r inMemoryAppStoreInterpreter = interpret $ \case CreateApp app -> modify (app :) - GetApp uid -> gets $ find $ \app -> app.id == uid + GetApp uid tid -> gets $ find $ \app -> app.id == uid && app.teamId == tid diff --git a/postgres-schema.sql b/postgres-schema.sql index 649dc4bf1f2..995fda76c15 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -51,7 +51,10 @@ SET default_table_access_method = heap; CREATE TABLE public.apps ( user_id uuid NOT NULL, team_id uuid NOT NULL, - metadata json + metadata json, + category text DEFAULT 'other'::text NOT NULL, + description text DEFAULT ''::text NOT NULL, + creator uuid NOT NULL ); diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index ce0031a70ef..05bf6b8f2ec 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -631,6 +631,7 @@ servantSitemap = appsAPI :: ServerT AppsAPI (Handler r) appsAPI = Named @"create-app" createApp + :<|> Named @"get-app" getApp :<|> Named @"refresh-app-cookie" refreshAppCookie --------------------------------------------------------------------------- @@ -1753,6 +1754,9 @@ checkUserGroupNameAvailable _ _ = pure $ UserGroupNameAvailability True createApp :: (_) => Local UserId -> TeamId -> NewApp -> Handler r CreatedApp createApp lusr tid new = lift . liftSem $ AppSubsystem.createApp lusr tid new +getApp :: (_) => Local UserId -> TeamId -> UserId -> Handler r GetApp +getApp lusr tid uid = lift . liftSem $ AppSubsystem.getApp lusr tid uid + refreshAppCookie :: (_) => Local UserId -> TeamId -> UserId -> Handler r RefreshAppCookieResponse refreshAppCookie lusr tid appId = do mc <- lift . liftSem $ AppSubsystem.refreshAppCookie lusr tid appId From 9471776d0753a8147dec6941459c7286e89883c9 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 9 Dec 2025 15:04:54 +0100 Subject: [PATCH 15/60] [WPB-21706] empty notification page with has_more = True (#4871) --- changelog.d/3-bug-fixes/WPB-21706 | 1 + integration/test/Test/MLS/Notifications.hs | 79 ++++++++++++++++++ libs/metrics-wai/src/Data/Metrics/Servant.hs | 6 +- .../gundeck/src/Gundeck/Notification/Data.hs | 80 +++++++++++-------- 4 files changed, 130 insertions(+), 36 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-21706 diff --git a/changelog.d/3-bug-fixes/WPB-21706 b/changelog.d/3-bug-fixes/WPB-21706 new file mode 100644 index 00000000000..81a14f6352c --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-21706 @@ -0,0 +1 @@ +Fixed notification endpoint returning an empty page with `hasMore=true` diff --git a/integration/test/Test/MLS/Notifications.hs b/integration/test/Test/MLS/Notifications.hs index d4c1eb4d0d8..5b5f87d1dc6 100644 --- a/integration/test/Test/MLS/Notifications.hs +++ b/integration/test/Test/MLS/Notifications.hs @@ -17,7 +17,11 @@ module Test.MLS.Notifications where +import API.Common (recipient) import API.Gundeck +import API.GundeckInternal (postPush) +import Control.Concurrent (threadDelay) +import Data.Timeout import MLS.Util import Notifications import SetupHelpers @@ -45,3 +49,78 @@ testWelcomeNotification = do size = Just 10000 } >>= getJSON 200 + +testNotificationPagination :: (HasCallStack) => App () +testNotificationPagination = do + let overrides = + def + { gundeckCfg = + setField "settings.maxPayloadLoadSize" (Just ((2 :: Int) * 1024)) + >=> setField "settings.notificationTTL" (2 #> Second) + } + withModifiedBackend overrides $ \dom -> do + user <- randomUser dom def + + liftIO $ threadDelay 2_100_000 -- let notifications expire + + -- Create a single oversized notification so Cassandra paging stops after the first row. + r <- recipient user + let bigPayload = replicate (3 * 1024) 'x' -- 3 KiB > maxPayloadLoadSize + push = + object + [ "recipients" .= [r], + "payload" .= [object ["blob" .= bigPayload]] + ] + + postPush user [push] >>= assertSuccess + + notifId <- + getNotifications user def `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + notif <- resp.json %. "notifications" >>= asList >>= assertOne + notif %. "id" >>= asString + + -- Re-request starting after that notification + getNotifications user def {since = Just notifId} + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "notifications" >>= asList >>= shouldBeEmpty + resp.json %. "has_more" `shouldMatch` False + +testNotificationPaginationOversizeSince :: (HasCallStack) => App () +testNotificationPaginationOversizeSince = do + let overrides = + def + { gundeckCfg = + setField "settings.maxPayloadLoadSize" (Just ((2 :: Int) * 1024)) + >=> setField "settings.notificationTTL" (2 #> Second) + } + withModifiedBackend overrides $ \dom -> do + user <- randomUser dom def + liftIO $ threadDelay 2_100_000 -- let notifications expire + r <- recipient user + let bigPayload = replicate (3 * 1024) 'x' + smallPayload = "ok" + mkPush payload = + object + [ "recipients" .= [r], + "payload" .= [object ["blob" .= payload]] + ] + + postPush user [mkPush bigPayload] >>= assertSuccess + + bigNotifId <- + getNotifications user def `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + notif <- resp.json %. "notifications" >>= asList >>= assertOne + notif %. "id" >>= asString + + -- Send a second, small notification that should show up after the anchor. + postPush user [mkPush smallPayload] >>= assertSuccess + + getNotifications user def {since = Just bigNotifId} + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "has_more" `shouldMatch` False + n <- resp.json %. "notifications" >>= asList >>= assertOne + n %. "payload.0.blob" `shouldMatch` "ok" diff --git a/libs/metrics-wai/src/Data/Metrics/Servant.hs b/libs/metrics-wai/src/Data/Metrics/Servant.hs index 3d3313ee2a0..c0a791fa499 100644 --- a/libs/metrics-wai/src/Data/Metrics/Servant.hs +++ b/libs/metrics-wai/src/Data/Metrics/Servant.hs @@ -28,8 +28,7 @@ module Data.Metrics.Servant where import Data.ByteString.UTF8 qualified as UTF8 import Data.Id -import Data.Metrics.Types -import Data.Metrics.Types qualified as Metrics +import Data.Metrics.Types as Metrics import Data.Proxy import Data.Text.Encoding import Data.Text.Encoding.Error @@ -37,8 +36,7 @@ import Data.Tree import GHC.TypeLits import Imports import Network.Wai qualified as Wai -import Network.Wai.Middleware.Prometheus -import Network.Wai.Middleware.Prometheus qualified as Promth +import Network.Wai.Middleware.Prometheus as Promth import Servant.API import Servant.Multipart diff --git a/services/gundeck/src/Gundeck/Notification/Data.hs b/services/gundeck/src/Gundeck/Notification/Data.hs index 06f294e345e..c8d9ba20f7e 100644 --- a/services/gundeck/src/Gundeck/Notification/Data.hs +++ b/services/gundeck/src/Gundeck/Notification/Data.hs @@ -34,7 +34,7 @@ import Data.Id import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NonEmpty import Data.Range (Range, fromRange) -import Data.Sequence (Seq, ViewL ((:<))) +import Data.Sequence (Seq) import Data.Sequence qualified as Seq import Gundeck.Env import Gundeck.Options (NotificationTTL (..), internalPageSize, maxPayloadLoadSize, settings) @@ -179,18 +179,26 @@ payloadSize (_, mbPayload, _, mbPayloadRefSize, _) = (_, Just size) -> size _ -> 0 +data FetchPayloadsResult = FetchPayloadsResult + { notifications :: Seq QueuedNotification, + remainingBytes :: Int32, + truncatedByPayloadLimit :: Bool + } + -- | Fetches referenced payloads until maxTotalSize payload bytes are fetched from the database. -- At least the first row is fetched regardless of the payload size. -fetchPayloads :: (MonadClient m, MonadUnliftIO m) => Maybe ClientId -> Int32 -> [NotifRow] -> m (Seq QueuedNotification, Int32) -fetchPayloads c left rows = do - let (rows', left') = truncateNotifs [] (0 :: Int) left rows - s <- Seq.fromList . catMaybes <$> pooledMapConcurrentlyN 16 (fetchPayload c) rows' - pure (s, left') +fetchPayloads :: (MonadClient m, MonadUnliftIO m) => Maybe ClientId -> Int32 -> [NotifRow] -> m FetchPayloadsResult +fetchPayloads client remainingBytes rows = do + let isFirstRow = True + let (rows', remainingBytes', truncatedByPayloadLimit) = truncateNotifs [] isFirstRow remainingBytes rows + notifications <- Seq.fromList . catMaybes <$> pooledMapConcurrentlyN 16 (fetchPayload client) rows' + pure $ FetchPayloadsResult notifications remainingBytes' truncatedByPayloadLimit where - truncateNotifs acc _i l [] = (reverse acc, l) - truncateNotifs acc i l (row : rest) - | i > 0 && l <= 0 = (reverse acc, l) - | otherwise = truncateNotifs (row : acc) (i + 1) (l - payloadSize row) rest + truncateNotifs :: [NotifRow] -> Bool -> Int32 -> [NotifRow] -> ([NotifRow], Int32, Bool) + truncateNotifs acc _ remainingBytes' [] = (reverse acc, remainingBytes', False) + truncateNotifs acc isFirstRow remainingBytes' (row : rest) + | not isFirstRow && remainingBytes' <= 0 = (reverse acc, remainingBytes', not (null rest)) + | otherwise = truncateNotifs (row : acc) False (remainingBytes' - payloadSize row) rest -- | Tries to fetch @remaining@ many notifications. -- The returned 'Seq' might contain more notifications than @remaining@, (see @@ -198,16 +206,21 @@ fetchPayloads c left rows = do -- -- The boolean indicates whether more notifications can be fetched. collect :: (MonadReader Env m, MonadClient m, MonadUnliftIO m) => Maybe ClientId -> Seq QueuedNotification -> Bool -> Int -> Int32 -> m (Page NotifRow) -> m (Seq QueuedNotification, Bool) -collect c acc lastPageHasMore remaining remainingBytes getPage - | remaining <= 0 = pure (acc, lastPageHasMore) - | remainingBytes <= 0 = pure (acc, True) - | not lastPageHasMore = pure (acc, False) +collect c acc prevPageHasMore remaining remainingBytes getPage + -- we have fetched at least the requested size: terminating the recursion + | remaining <= 0 = pure (acc, prevPageHasMore) + -- we reached or exceeded the max payload: terminating the recursion + | remainingBytes <= 0 = pure (acc, prevPageHasMore) + -- there is no more data: terminating the recursion + | not prevPageHasMore = pure (acc, False) + -- in any other case we are going to get the next page | otherwise = do page <- getPage let rows = result page - (s, remaingBytes') <- fetchPayloads c remainingBytes rows - let remaining' = remaining - Seq.length s - collect c (acc <> s) (hasMore page) remaining' remaingBytes' (liftClient (nextPage page)) + fetchResult <- fetchPayloads c remainingBytes rows + let remaining' = remaining - Seq.length fetchResult.notifications + more' = hasMore page || fetchResult.truncatedByPayloadLimit + collect c (acc <> fetchResult.notifications) more' remaining' fetchResult.remainingBytes (liftClient (nextPage page)) mkResultPage :: Int -> Bool -> Seq QueuedNotification -> ResultPage mkResultPage size more ns = @@ -224,7 +237,8 @@ fetch u c Nothing (fromIntegral . fromRange -> size) = do -- We always need to look for one more than requested in order to correctly -- report whether there are more results. maxPayloadSize <- fromMaybe (5 * 1024 * 1024) <$> asks (^. options . settings . maxPayloadLoadSize) - (ns, more) <- collect c Seq.empty True (size + 1) maxPayloadSize page1 + let prevPageHasMore = True + (ns, more) <- collect c Seq.empty prevPageHasMore (size + 1) maxPayloadSize page1 -- Drop the extra element at the end if present pure $! mkResultPage size more ns where @@ -236,29 +250,31 @@ fetch u c Nothing (fromIntegral . fromRange -> size) = do \ORDER BY id ASC" fetch u c (Just since) (fromIntegral . fromRange -> size) = do pageSize <- fromMaybe 100 <$> asks (^. options . settings . internalPageSize) + sinceFound <- isJust <$> retry x1 (query1 cqlExists (params LocalQuorum (u, TimeUuid (toUUID since)))) let page1 = retry x1 $ - paginate cqlSince (paramsP LocalQuorum (u, TimeUuid (toUUID since)) pageSize) - -- We fetch 2 more rows than requested. The first is to accommodate the - -- notification corresponding to the `since` argument itself. The second is - -- to get an accurate `hasMore`, just like in the case above. - + paginate cqlAfterSince (paramsP LocalQuorum (u, TimeUuid (toUUID since)) pageSize) maxPayloadSize <- fromMaybe (5 * 1024 * 1024) <$> asks (^. options . settings . maxPayloadLoadSize) - (ns, more) <- collect c Seq.empty True (size + 2) maxPayloadSize page1 - -- Remove notification corresponding to the `since` argument, and record if it is found. - let (ns', sinceFound) = case Seq.viewl ns of - x :< xs | since == x ^. queuedNotificationId -> (xs, True) - _ -> (ns, False) + let prevPageHasMore = True + -- We always need to look for one more than requested in order to correctly + -- report whether there are more results. + (ns, more) <- collect c Seq.empty prevPageHasMore (size + 1) maxPayloadSize page1 pure $! - (mkResultPage size more ns') + (mkResultPage size more ns) { resultGap = not sinceFound } where - cqlSince :: PrepQuery R (UserId, TimeUuid) NotifRow - cqlSince = + cqlExists :: PrepQuery R (UserId, TimeUuid) (Identity TimeUuid) + cqlExists = + "SELECT id \ + \FROM notifications \ + \WHERE user = ? AND id = ?" + + cqlAfterSince :: PrepQuery R (UserId, TimeUuid) NotifRow + cqlAfterSince = "SELECT id, payload, payload_ref, payload_ref_size, clients \ \FROM notifications \ - \WHERE user = ? AND id >= ? \ + \WHERE user = ? AND id > ? \ \ORDER BY id ASC" deleteAll :: (MonadClient m) => UserId -> m () From a5c6e7c924c5c78d657d075eed7b839ff9c5645d Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 9 Dec 2025 15:50:27 +0100 Subject: [PATCH 16/60] Optimize Postgresql queries for getting converstaion members (#4896) * Wire.Postgres: Introduce runSesssion This will allow running multiple statements in the same session * ConversationStore.Postgres: Remove use of `OR` and expsensive sorting when getting members Using `OR` makes the index usage less efficient. The ordering is also very expensive for postgresql. --- .../3-bug-fixes/optimize-conv-member-queries | 1 + .../src/Wire/ConversationStore/Postgres.hs | 120 +++++++++++------- libs/wire-subsystems/src/Wire/Postgres.hs | 24 ++-- 3 files changed, 92 insertions(+), 53 deletions(-) create mode 100644 changelog.d/3-bug-fixes/optimize-conv-member-queries diff --git a/changelog.d/3-bug-fixes/optimize-conv-member-queries b/changelog.d/3-bug-fixes/optimize-conv-member-queries new file mode 100644 index 00000000000..329c12ab249 --- /dev/null +++ b/changelog.d/3-bug-fixes/optimize-conv-member-queries @@ -0,0 +1 @@ +Optimize Postgresql queries for getting conversation members \ No newline at end of file diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs index 4a668eaa809..f14ddb30b74 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs @@ -35,6 +35,7 @@ import GHC.Records (HasField) import Hasql.Decoders qualified as HD import Hasql.Pipeline qualified as Pipeline import Hasql.Pool qualified as Hasql +import Hasql.Session qualified as HasqlSession import Hasql.Statement qualified as Hasql import Hasql.TH import Hasql.Transaction (Transaction) @@ -203,8 +204,8 @@ getConversationImpl cid = case mConvRow of Nothing -> pure Nothing Just convRow -> do - localMembers <- Transaction.statement cid selectLocalMembersStmt - remoteMembers <- Transaction.statement cid selectRemoteMembersStmt + localMembers <- dedupMembers cid <$> Transaction.statement cid selectLocalMembersStmt + remoteMembers <- dedupMembers cid <$> Transaction.statement cid selectRemoteMembersStmt pure $ toConv cid localMembers remoteMembers (Just convRow) selectConvMetadata :: Hasql.Statement (ConvId) (Maybe ConvRow) @@ -646,7 +647,13 @@ createBotMemberImpl serviceRef botId convId = do getLocalMemberImpl :: (PGConstraints r) => ConvId -> UserId -> Sem r (Maybe LocalMember) getLocalMemberImpl convId userId = do - mRow <- runStatement (convId, userId) selectMember + mRow <- + runSession $ do + mDirectMember <- HasqlSession.statement (convId, userId) selectMember + case mDirectMember of + Nothing -> HasqlSession.statement (convId, userId) selectParentMember + Just mem -> pure (Just mem) + pure $ snd . mkLocalMember <$> mRow where selectMember :: Hasql.Statement (ConvId, UserId) (Maybe (ConvId, UserId, Maybe ServiceId, Maybe ProviderId, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName)) @@ -655,30 +662,44 @@ getLocalMemberImpl convId userId = do [maybeStatement|SELECT (conv :: uuid), ("user" :: uuid), (service :: uuid?), (provider :: uuid?), (otr_muted_status :: integer?), (otr_muted_ref :: text?), (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) FROM conversation_member - WHERE (conv = ($1 :: uuid) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid))) + WHERE conv = ($1 :: uuid) + AND "user" = ($2 :: uuid) + |] + + selectParentMember :: Hasql.Statement (ConvId, UserId) (Maybe (ConvId, UserId, Maybe ServiceId, Maybe ProviderId, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName)) + selectParentMember = + dimapPG + [maybeStatement|SELECT (conv :: uuid), ("user" :: uuid), (service :: uuid?), (provider :: uuid?), (otr_muted_status :: integer?), (otr_muted_ref :: text?), + (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) + FROM conversation_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) AND "user" = ($2 :: uuid) - ORDER BY CASE - WHEN conv = ($1 :: uuid) THEN 1 - ELSE 2 - END - LIMIT 1 |] getLocalMembersImpl :: (PGConstraints r) => ConvId -> Sem r [LocalMember] getLocalMembersImpl convId = - runStatement convId selectLocalMembersStmt + dedupMembers convId <$> runStatement convId selectLocalMembersStmt type LocalMemberRow = (ConvId, UserId, Maybe ServiceId, Maybe ProviderId, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName) -selectLocalMembersStmt :: Hasql.Statement ConvId [LocalMember] +-- | A user can be member of a conv and its parent. If the user is only part of +-- the one of these, we return that member object. If the user is part of both, +-- we return the one corresponding to the conversation being queried. +dedupMembers :: (HasField "id_" mem a, Ord a) => ConvId -> [(ConvId, mem)] -> [mem] +dedupMembers convId memsWithConvIds = + let sortFunction (cid1, mem1) (cid2, mem2) + | cid1 == cid2 = EQ + | cid1 == convId = LT + | cid2 == convId = GT + | otherwise = compare (cid1, mem1.id_) (cid2, mem2.id_) + orderedMems = snd <$> sortBy sortFunction memsWithConvIds + in nubBy ((==) `on` (.id_)) orderedMems + +-- | The members must be deduped using 'dedupMembers' before use +selectLocalMembersStmt :: Hasql.Statement ConvId [(ConvId, LocalMember)] selectLocalMembersStmt = - dedupMembers <$> select + mkLocalMember <$$> select where - dedupMembers rows = - let localMembers = mkLocalMember <$> rows - in map snd $ nubBy ((==) `on` ((.id_) . snd)) localMembers - select :: Hasql.Statement ConvId [LocalMemberRow] select = dimapPG @@ -686,11 +707,13 @@ selectLocalMembersStmt = (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) FROM conversation_member WHERE conv = ($1 :: uuid) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) - ORDER BY CASE - WHEN conv = ($1 :: uuid) THEN 1 - ELSE 2 - END + + UNION ALL + + SELECT (conv :: uuid), ("user" :: uuid), (service :: uuid?), (provider :: uuid?), (otr_muted_status :: integer?), (otr_muted_ref :: text?), + (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) + FROM conversation_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) |] mkLocalMember :: LocalMemberRow -> (ConvId, LocalMember) @@ -712,8 +735,15 @@ mkLocalMember (cid, uid, mServiceId, mProviderId, msOtrMutedStatus, msOtrMutedRe type RemoteMemberRow = (ConvId, Domain, UserId, RoleName) getRemoteMemberImpl :: (PGConstraints r) => ConvId -> Remote UserId -> Sem r (Maybe RemoteMember) -getRemoteMemberImpl convId (tUntagged -> Qualified uid domain) = - snd . mkRemoteMember <$$> runStatement (convId, domain, uid) selectMember +getRemoteMemberImpl convId (tUntagged -> Qualified uid domain) = do + mRow <- + runSession $ do + mDirectMember <- HasqlSession.statement (convId, domain, uid) selectMember + case mDirectMember of + Nothing -> HasqlSession.statement (convId, domain, uid) selectParentMember + Just mem -> pure (Just mem) + + pure $ snd . mkRemoteMember <$> mRow where selectMember :: Hasql.Statement (ConvId, Domain, UserId) (Maybe RemoteMemberRow) selectMember = @@ -722,38 +752,40 @@ getRemoteMemberImpl convId (tUntagged -> Qualified uid domain) = FROM local_conversation_remote_member WHERE user_remote_domain = ($2 :: text) AND user_remote_id = ($3 :: uuid) - AND (conv = ($1 :: uuid) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid))) - ORDER BY CASE - WHEN conv = ($1 :: uuid) THEN 1 - ELSE 2 - END - LIMIT 1 + AND conv = ($1 :: uuid) + |] + + selectParentMember :: Hasql.Statement (ConvId, Domain, UserId) (Maybe RemoteMemberRow) + selectParentMember = + dimapPG + [maybeStatement|SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) + FROM local_conversation_remote_member + WHERE user_remote_domain = ($2 :: text) + AND user_remote_id = ($3 :: uuid) + AND conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) |] getRemoteMembersImpl :: (PGConstraints r) => ConvId -> Sem r [RemoteMember] getRemoteMembersImpl convId = - runStatement convId selectRemoteMembersStmt + dedupMembers convId <$> runStatement convId selectRemoteMembersStmt -selectRemoteMembersStmt :: Hasql.Statement ConvId [RemoteMember] +-- | The members must be deduped using 'dedupMembers' before use +selectRemoteMembersStmt :: Hasql.Statement ConvId [(ConvId, RemoteMember)] selectRemoteMembersStmt = - dedupMembers <$> select + mkRemoteMember <$$> select where - dedupMembers rows = - let localMembers = mkRemoteMember <$> rows - in map snd $ nubBy ((==) `on` ((.id_) . snd)) localMembers - select :: Hasql.Statement ConvId [(ConvId, Domain, UserId, RoleName)] select = dimapPG [vectorStatement|SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) FROM local_conversation_remote_member - WHERE (conv = ($1 :: uuid) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid))) - ORDER BY CASE - WHEN conv = ($1 :: uuid) THEN 1 - ELSE 2 - END + WHERE conv = ($1 :: uuid) + + UNION ALL + + SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) + FROM local_conversation_remote_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) |] mkRemoteMember :: (ConvId, Domain, UserId, RoleName) -> (ConvId, RemoteMember) diff --git a/libs/wire-subsystems/src/Wire/Postgres.hs b/libs/wire-subsystems/src/Wire/Postgres.hs index 4629e56dbdd..746bd701624 100644 --- a/libs/wire-subsystems/src/Wire/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/Postgres.hs @@ -38,6 +38,7 @@ module Wire.Postgres -- * Runners runStatement, + runSession, runTransaction, runPipeline, parseCount, @@ -91,14 +92,21 @@ type PGConstraints r = Member (Error Hasql.UsageError) r ) +runSession :: + (PGConstraints r) => + Session a -> + Sem r a +runSession sess = do + pool <- input + liftIO (use pool sess) >>= either throw pure + runStatement :: (PGConstraints r) => a -> Statement a b -> Sem r b -runStatement a stmt = do - pool <- input - liftIO (use pool (statement a stmt)) >>= either throw pure +runStatement a stmt = + runSession $ statement a stmt runTransaction :: (PGConstraints r) => @@ -106,17 +114,15 @@ runTransaction :: Mode -> Transaction a -> Sem r a -runTransaction isolationLevel mode t = do - pool <- input - liftIO (use pool $ Transaction.transaction isolationLevel mode t) >>= either throw pure +runTransaction isolationLevel mode t = + runSession $ Transaction.transaction isolationLevel mode t runPipeline :: (PGConstraints r) => Pipeline a -> Sem r a -runPipeline p = do - pool <- input - liftIO (use pool $ pipeline p) >>= either throw pure +runPipeline p = + runSession $ pipeline p class PostgresValue a where postgresType :: Text From 72b58d00367631d6268594b5a079ef06c0532487 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 10 Dec 2025 16:19:22 +0100 Subject: [PATCH 17/60] Reduce gc_grace_period for all conversation related tables to 1 day (#4899) --- changelog.d/3-bug-fixes/conv-gc-grace-period | 3 ++ services/galley/galley.cabal | 1 + services/galley/src/Galley/Schema/Run.hs | 4 +- .../V101_ConversationLowerGCGracePeriod.hs | 48 +++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3-bug-fixes/conv-gc-grace-period create mode 100644 services/galley/src/Galley/Schema/V101_ConversationLowerGCGracePeriod.hs diff --git a/changelog.d/3-bug-fixes/conv-gc-grace-period b/changelog.d/3-bug-fixes/conv-gc-grace-period new file mode 100644 index 00000000000..8fe81db89d8 --- /dev/null +++ b/changelog.d/3-bug-fixes/conv-gc-grace-period @@ -0,0 +1,3 @@ +Reduce gc_grace_period for all conversation related tables to 1 day. This will +help restart the postgresql migration after a day, if it fails mid way. Lowering +it too much runs the risk of offline nodes resurrecting deleted data. \ No newline at end of file diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 59bfdc5f121..8f8d45f27d6 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -170,6 +170,7 @@ library Galley.Run Galley.Schema.Run Galley.Schema.V100_OutOfSync + Galley.Schema.V101_ConversationLowerGCGracePeriod Galley.Schema.V20 Galley.Schema.V21 Galley.Schema.V22 diff --git a/services/galley/src/Galley/Schema/Run.hs b/services/galley/src/Galley/Schema/Run.hs index b32b21b98b7..f9b07735f85 100644 --- a/services/galley/src/Galley/Schema/Run.hs +++ b/services/galley/src/Galley/Schema/Run.hs @@ -21,6 +21,7 @@ import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) import Galley.Schema.V100_OutOfSync qualified as V100_OutOfSync +import Galley.Schema.V101_ConversationLowerGCGracePeriod qualified as V101_ConversationLowerGCGracePeriod import Galley.Schema.V20 qualified as V20 import Galley.Schema.V21 qualified as V21 import Galley.Schema.V22 qualified as V22 @@ -202,7 +203,8 @@ migrations = V97_CellsConversation.migration, V98_ChannelAddPermission.migration, V99_ConversationAddParent.migration, - V100_OutOfSync.migration + V100_OutOfSync.migration, + V101_ConversationLowerGCGracePeriod.migration -- FUTUREWORK: once #1726 has made its way to master/production, -- the 'message' field in connections table can be dropped. -- See also https://github.com/wireapp/wire-server/pull/1747/files diff --git a/services/galley/src/Galley/Schema/V101_ConversationLowerGCGracePeriod.hs b/services/galley/src/Galley/Schema/V101_ConversationLowerGCGracePeriod.hs new file mode 100644 index 00000000000..0ec66d5905e --- /dev/null +++ b/services/galley/src/Galley/Schema/V101_ConversationLowerGCGracePeriod.hs @@ -0,0 +1,48 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 Galley.Schema.V101_ConversationLowerGCGracePeriod + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 101 "set gc_grace_period for conversation related tables to 1 day" $ do + schema' + [r| ALTER TABLE conversation WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE mls_group_member_client WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE subconversation WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE member WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE user WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE member_remote_user WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE team_conv WITH gc_grace_seconds = 86400 |] From e8a28dc21f3a1514584dec8146fb212509d5e988 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 11 Dec 2025 10:46:12 +0100 Subject: [PATCH 18/60] [WPB-22154] fix: move user between SCIM tokens (#4887) * Add: failing integration test. * Fix: don't drop saml changes in scim user update/patch on the floor. * Fix: integration tests * Drive-by fixes & tweaks. * Haddocks. * Make postgres schema dumping deterministic. * Simplify email selection semantics in newVeidFromBrigUser. It's slightly less insane now, but it does change behavior in some (untested) corner cases. --- ...PB-22154-fix-move-user-between-scim-tokens | 1 + hack/bin/postgres_dump_schema | 15 ++- libs/wire-api/src/Wire/API/User/Identity.hs | 17 ++- libs/wire-api/src/Wire/API/User/Scim.hs | 2 +- postgres-schema.sql | 11 +- services/spar/src/Spar/Intra/BrigApp.hs | 47 +++++--- services/spar/src/Spar/Scim/Auth.hs | 4 +- services/spar/src/Spar/Scim/User.hs | 46 ++++---- .../Test/Spar/Scim/UserSpec.hs | 111 +++++++++++++++++- services/spar/test-integration/Util/Scim.hs | 25 ++-- 10 files changed, 205 insertions(+), 74 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-22154-fix-move-user-between-scim-tokens diff --git a/changelog.d/3-bug-fixes/WPB-22154-fix-move-user-between-scim-tokens b/changelog.d/3-bug-fixes/WPB-22154-fix-move-user-between-scim-tokens new file mode 100644 index 00000000000..4ff779c2447 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-22154-fix-move-user-between-scim-tokens @@ -0,0 +1 @@ +Fixed: change user idp, external_id or emails via scim (scim user update / patch failed to update parts of `ValidScimId`). \ No newline at end of file diff --git a/hack/bin/postgres_dump_schema b/hack/bin/postgres_dump_schema index be9c3220f50..b7435b41fc9 100755 --- a/hack/bin/postgres_dump_schema +++ b/hack/bin/postgres_dump_schema @@ -4,6 +4,7 @@ import sys import subprocess from subprocess import PIPE import re +import hashlib def run_psql(container, expr, dbname="postgres", user="wire-server"): p = ( @@ -50,6 +51,17 @@ def list_databases(container, user="wire-server"): dbs.append(match.group(1)) return dbs +def normalize_rls_hashes(dump_output, dbname): + """Replace random RLS policy hashes with deterministic ones based on database name.""" + # Create a deterministic hash based on the database name + det_hash = hashlib.sha256(dbname.encode()).hexdigest()[:63] + + # Replace both \restrict and \unrestrict hashes + output = re.sub(r'\\restrict [A-Za-z0-9]+', f'\\\\restrict {det_hash}', dump_output) + output = re.sub(r'\\unrestrict [A-Za-z0-9]+', f'\\\\unrestrict {det_hash}', output) + + return output + def main(): container = get_container_id() print("-- automatically generated with `make postgres-schema`") @@ -65,7 +77,8 @@ def main(): for db in databases: print(f"\n------------------------------------------------------------------------------------------") print(f"-- Database: {db}\n") - print(run_pg_dump(container, db, user="wire-server")) + dump = run_pg_dump(container, db, user="wire-server") + print(normalize_rls_hashes(dump, db)) if __name__ == "__main__": main() diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index 841f6dc025d..97a3c503e59 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -203,16 +203,16 @@ instance ToJSON UserSSOId where instance FromJSON UserSSOId where parseJSON = A.withObject "UserSSOId" $ \obj -> do - mtenant <- lenientlyParseSAMLIssuer =<< (obj A..:? "tenant") - msubject <- lenientlyParseSAMLNameID =<< (obj A..:? "subject") + mtenant <- mapM lenientlyParseSAMLIssuer =<< (obj A..:? "tenant") + msubject <- mapM lenientlyParseSAMLNameID =<< (obj A..:? "subject") meid <- obj A..:? "scim_external_id" case (mtenant, msubject, meid) of (Just tenant, Just subject, Nothing) -> pure $ UserSSOId (SAML.UserRef tenant subject) (Nothing, Nothing, Just eid) -> pure $ UserScimExternalId eid _ -> fail "either need tenant and subject, or scim_external_id, but not both" -lenientlyParseSAMLIssuer :: Maybe LText -> A.Parser (Maybe SAML.Issuer) -lenientlyParseSAMLIssuer mbtxt = forM mbtxt $ \txt -> do +lenientlyParseSAMLIssuer :: LText -> A.Parser SAML.Issuer +lenientlyParseSAMLIssuer txt = do let asxml :: Either String SAML.Issuer asxml = SAML.decodeElem txt @@ -222,13 +222,12 @@ lenientlyParseSAMLIssuer mbtxt = forM mbtxt $ \txt -> do URI.parseURI URI.laxURIParserOptions (encodeUtf8 . LT.toStrict $ txt) err :: String - err = "lenientlyParseSAMLIssuer: " <> show (asxml, asurl, mbtxt) + err = "lenientlyParseSAMLIssuer: " <> show (asxml, asurl, txt) maybe (fail err) pure $ hush asxml <|> hush asurl -lenientlyParseSAMLNameID :: Maybe LText -> A.Parser (Maybe SAML.NameID) -lenientlyParseSAMLNameID Nothing = pure Nothing -lenientlyParseSAMLNameID (Just txt) = do +lenientlyParseSAMLNameID :: LText -> A.Parser SAML.NameID +lenientlyParseSAMLNameID txt = do let asxml :: Either String SAML.NameID asxml = SAML.decodeElem txt @@ -249,7 +248,7 @@ lenientlyParseSAMLNameID (Just txt) = do maybe (fail err) - (pure . Just) + pure (hush asxml <|> hush asemail <|> hush astxt) -- | For testing. Create a sample 'SAML.UserRef' value with random seeds to make 'Issuer' and diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 1c83919a1b3..174a4ef6b1b 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -425,7 +425,7 @@ data CreateScimToken = CreateScimToken verificationCode :: !(Maybe Code.Value), -- | Optional name for the token name :: Maybe Text, - -- | Optional IdP that created users will "belong" to + -- | Optional SAML IdP that created users will "belong" to; if Nothing, do not use SAML. idp :: Maybe SAML.IdPId } deriving (Eq, Show, Generic) diff --git a/postgres-schema.sql b/postgres-schema.sql index 995fda76c15..378195989b4 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -7,7 +7,7 @@ -- PostgreSQL database dump -- -\restrict 7mIWfK3KzS3dh5XHiAV6Z4AKzhK7lHVugezBtZY2vcFi7gDC9DiStSTii9adK62 +\restrict 79bbfb4630959c48307653a5cd3d83f2582b3c2210f75f10d79e3ebf0015620 -- Dumped from database version 17.6 -- Dumped by pg_dump version 17.6 @@ -399,6 +399,13 @@ CREATE INDEX collaborators_user_id_idx ON public.collaborators USING btree (user CREATE INDEX conversation_member_user_idx ON public.conversation_member USING btree ("user"); +-- +-- Name: conversation_team_group_type_lower_name_id_idx; Type: INDEX; Schema: public; Owner: wire-server +-- + +CREATE INDEX conversation_team_group_type_lower_name_id_idx ON public.conversation USING btree (team, group_conv_type, lower(name), id); + + -- -- Name: conversation_team_idx; Type: INDEX; Schema: public; Owner: wire-server -- @@ -480,4 +487,4 @@ REVOKE USAGE ON SCHEMA public FROM PUBLIC; -- PostgreSQL database dump complete -- -\unrestrict 7mIWfK3KzS3dh5XHiAV6Z4AKzhK7lHVugezBtZY2vcFi7gDC9DiStSTii9adK62 +\unrestrict 79bbfb4630959c48307653a5cd3d83f2582b3c2210f75f10d79e3ebf0015620 diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index b14a3a60b26..08bc096ee87 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -23,7 +23,8 @@ module Spar.Intra.BrigApp ( veidToUserSSOId, urefToExternalId, - veidFromBrigUser, + oldVeidFromBrigUser, + newVeidFromBrigUser, veidFromUserSSOId, mkUserName, HavePendingInvitations (..), @@ -42,6 +43,7 @@ import Brig.Types.Intra import Control.Lens import Control.Monad.Except import Data.ByteString.Conversion +import Data.CaseInsensitive (original) import qualified Data.CaseInsensitive as CI import Data.Handle (Handle, parseHandle) import Data.HavePendingInvitations @@ -88,26 +90,39 @@ veidFromUserSSOId ssoId mEmail = case ssoId of -- If veid can be parsed as an email, we end up in the case above with email delivered separately. throwError "internal error: externalId is not an email and there is no SAML issuer" --- | If the brig user has a 'UserSSOId', transform that into a 'ValidScimId' (this is a --- total function as long as brig obeys the api). Otherwise, if the user has an email, we can --- construct a return value from that (and an optional saml issuer). +-- | Turns ssoid and email* fields back into a `ValidScimId`. +oldVeidFromBrigUser :: User -> Maybe ValidScimId +oldVeidFromBrigUser usr = + let mbEmail = userEmail usr <|> userEmailUnvalidated usr + in fromRight (error "impossible") $ (`veidFromUserSSOId` mbEmail) `mapM` userSSOId usr + +-- | Compute ValidScimId from updates. Take both the old user (just +-- like `oldVeidFromBrigUser`) and updated idp issuer and unvalidated +-- email into consideration. -- --- Note: the saml issuer is only needed in the case where a user has been invited via team --- settings and is now onboarded to saml/scim. If this case can safely be ruled out, it's ok --- to just set it to 'Nothing'. +-- If updated values are `Nothing`, the corresponding data from brig +-- user will be ignored (this is how you delete an idp association). -- --- `userSSOId usr` can be empty if the user has no SAML credentials and is brought under scim --- management for the first time. In that case, the externalId is taken to --- be the email address. -veidFromBrigUser :: (MonadError String m) => User -> Maybe SAML.Issuer -> Maybe EmailAddress -> m ValidScimId -veidFromBrigUser usr mIssuer mUnvalidatedEmail = case (userSSOId usr, userEmail usr, mIssuer) of - (Just ssoid, mValidatedEmail, _) -> do - -- `mEmail` is in synch with SCIM user schema. - let mEmail = mUnvalidatedEmail <|> mValidatedEmail - veidFromUserSSOId ssoid mEmail +-- `userSSOId usr` can be empty if the user has no SAML credentials +-- and is brought under scim management for the first time. In that +-- case, the externalId is taken to be the email address. +newVeidFromBrigUser :: (MonadError String m) => User -> Maybe SAML.Issuer -> m ValidScimId +newVeidFromBrigUser usr mIssuer = case (userSSOId usr, userEmail usr <|> userEmailUnvalidated usr, mIssuer) of + (Just ssoid, mbEmail, _) -> do + -- this makes sure email encoded in ssoid is in synch with SCIM user. + veidFromUserSSOId (updateSsoid ssoid) mbEmail (Nothing, Just email, Just issuer) -> pure $ ValidScimId (fromEmail email) (These email (SAML.UserRef issuer (fromRight' $ emailToSAMLNameID email))) (Nothing, Just email, Nothing) -> pure $ ValidScimId (fromEmail email) (This email) (Nothing, Nothing, _) -> throwError "user has neither ssoIdentity nor userEmail" + where + updateSsoid :: UserSSOId -> UserSSOId + updateSsoid ssoid = case (ssoid, mIssuer) of + (UserSSOId uref, Nothing) -> UserScimExternalId (uref ^. SAML.uidSubject . to SAML.nameIDToST . to original) + (dontchange@(UserScimExternalId _), Nothing) -> dontchange + (UserSSOId uref, Just issuer) -> UserSSOId (uref & SAML.uidTenant .~ issuer) + (UserScimExternalId eid, Just issuer) -> + let nameId :: SAML.NameID = SAML.emailNameID eid & fromRight (SAML.unspecifiedNameID eid) + in UserSSOId (SAML.UserRef issuer nameId) -- | Take a maybe text, construct a 'Name' from what we have in a scim user. If the text -- isn't present, use an email address or a saml subject (usually also an email address). If diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 09478cfa516..1f7b824ce10 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -181,8 +181,8 @@ createScimToken :: Sem r CreateScimTokenResponse createScimToken zusr Api.CreateScimToken {..} = do teamid <- guardScimTokenCreation zusr password verificationCode - mIdPId <- maybe (pure Nothing) (\idpid -> IdPConfigStore.getConfig idpid $> Just idpid) idp - createScimTokenUnchecked teamid name description mIdPId + let guardIdPExists = mapM_ IdPConfigStore.getConfig in guardIdPExists idp + createScimTokenUnchecked teamid name description idp guardScimTokenCreation :: forall r. diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index c1dcb6ddfe5..aaf4d56d418 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -851,7 +851,15 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = deleteUserInSpar account = do mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP - case Brig.veidFromBrigUser account ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) account.userEmailUnvalidated of + -- delete user with idp associated *before* this update. + case Brig.oldVeidFromBrigUser account of + Nothing -> pure () + Just veid -> lift $ do + for_ (justThere veid.validScimIdAuthInfo) (SAMLUserStore.delete uid) + ScimExternalIdStore.delete stiTeam veid.validScimIdExternal + + -- delete user with idp associated to current scim token. + case Brig.newVeidFromBrigUser account ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) of Left _ -> pure () Right veid -> lift $ do for_ (justThere veid.validScimIdAuthInfo) (SAMLUserStore.delete uid) @@ -1101,34 +1109,32 @@ getUserById :: MaybeT (Scim.ScimHandler (Sem r)) (Scim.StoredUser ST.SparTag) getUserById midp stiTeam uid = do brigUser <- MaybeT . lift $ BrigAccess.getAccount Brig.WithPendingInvitations uid - let mbveid = - Brig.veidFromBrigUser - brigUser - ((^. SAML.idpMetadata . SAML.edIssuer) <$> midp) - brigUser.userEmailUnvalidated - case mbveid of + let mbOldVeid = Brig.oldVeidFromBrigUser brigUser + mbNewVeid = Brig.newVeidFromBrigUser brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> midp) + case mbNewVeid of Right veid | userTeam brigUser == Just stiTeam -> lift $ do storedUser :: Scim.StoredUser ST.SparTag <- synthesizeStoredUser brigUser veid -- if we get a user from brig that hasn't been touched by scim yet, we call this -- function to move it under scim control. assertExternalIdNotUsedElsewhere stiTeam veid uid + handleVeidChange brigUser mbOldVeid veid createValidScimUserSpar stiTeam uid storedUser veid - lift $ do - when (veidChanged brigUser veid) $ - BrigAccess.setSSOId uid (veidToUserSSOId veid) - when (managedByChanged brigUser) $ - BrigAccess.setManagedBy uid ManagedByScim pure storedUser _ -> Applicative.empty where - veidChanged :: User -> ST.ValidScimId -> Bool - veidChanged usr veid = case userIdentity usr of - Nothing -> True - Just (EmailIdentity _) -> True - Just (SSOIdentity ssoid _) -> Brig.veidToUserSSOId veid /= ssoid - - managedByChanged :: User -> Bool - managedByChanged usr = userManagedBy usr /= ManagedByScim + handleVeidChange :: User -> Maybe ValidScimId -> ValidScimId -> Scim.ScimHandler (Sem r) () + handleVeidChange brigUser mbOldVeid newVeid = do + -- set sso_id + when (mbOldVeid /= Just newVeid) do + lift $ BrigAccess.setSSOId uid (veidToUserSSOId newVeid) + -- set managed_by + when (userManagedBy brigUser /= ManagedByScim) do + lift $ BrigAccess.setManagedBy uid ManagedByScim + -- remove dangling entry from spar.user_v2 table (cassandra) + case mbOldVeid of + Just oldVeid | ST.veidUref newVeid /= ST.veidUref oldVeid -> do + lift $ SAMLUserStore.delete uid `mapM_` ST.veidUref oldVeid + _ -> pure () scimFindUserByHandle :: forall r. diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 965a854b60c..48a631f3a27 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -41,12 +41,13 @@ import qualified Data.Aeson as Aeson import Data.Aeson.Lens (key, _String) import Data.Aeson.QQ (aesonQQ) import Data.Aeson.Types (fromJSON, toJSON) +import Data.ByteString (toStrict) import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI import qualified Data.Csv as Csv import Data.Handle (Handle, fromHandle, parseHandle, parseHandleEither) import Data.HavePendingInvitations -import Data.Id (TeamId, UserId, randomId) +import Data.Id (TeamId, UserId, idToText, randomId) import Data.Ix (inRange) import Data.LanguageCodes (ISO639_1 (..)) import Data.Misc (HttpsUrl, mkHttpsUrl) @@ -202,6 +203,101 @@ specImportToScimFromSAML = (Just !uid') <- createViaSaml idp privCreds uref liftIO $ uid' `shouldBe` uid + -- create new scim token detached from saml + tok2 :: ScimToken <- do + registerScimToken teamid Nothing + + -- sync user + storedUserGot2 <- do + resp <- + aFewTimes (getUser_ (Just tok2) uid =<< view teSpar) ((== 200) . statusCode) + >= \(Just acc) -> liftIO $ do + userIdentity acc + `shouldBe` let emailText :: Text = decodeUtf8 $ toStrict $ toByteString email + in Just (SSOIdentity (UserScimExternalId emailText) (Just email)) + + -- the idp gets deleted now, but we'll see below that the user account survives. + call $ callIdpDelete (env ^. teSpar) (Just owner) (idp ^. SAML.idpId) + + -- password reset should work + let newPassword :: Text = "a8b7c1d8-d425-11f0-abbb-637eaf3793ee" + passwdReset env email + passResetToken <- stealPasswdResetToken env email + passwdResetComplete env email passResetToken newPassword + + -- ... after that, password login should work + login env (Aeson.object ["email" Aeson..= email, "password" Aeson..= newPassword]) + + -- after changing scim external id, login still works. + let patchOp = PatchOp.PatchOp [replaceAttrib "externalId" (idToText uid)] + where + replaceAttrib :: Text -> Text -> PatchOp.Operation + replaceAttrib name value = + PatchOp.Operation + PatchOp.Replace + (Just (PatchOp.NormalPath (Filter.topLevelAttrPath name))) + (Just (toJSON value)) + in do + patchUser_ (Just tok2) (Just uid) patchOp (env ^. teSpar) !!! const 200 === statusCode + login env (Aeson.object ["email" Aeson..= email, "password" Aeson..= ("a8b7c1d8-d425-11f0-abbb-637eaf3793ee" :: Text)]) + + passwdReset :: TestEnv -> EmailAddress -> (MonadReader TestEnv m, MonadIO m) => m () + passwdReset env email = + void . call . post $ + (versioned "v13") + . (env ^. teBrig) + . path "/password-reset" + . contentJson + . json (Aeson.object ["email" Aeson..= email]) + . expect2xx + + passwdResetComplete :: TestEnv -> EmailAddress -> Text -> Text -> (MonadReader TestEnv m, MonadIO m) => m () + passwdResetComplete env email passResetToken password = + void . call . post $ + (versioned "v13") + . (env ^. teBrig) + . path "/password-reset/complete" + . contentJson + . json + ( Aeson.object + [ "email" Aeson..= email, + "code" Aeson..= passResetToken, + "password" Aeson..= password + ] + ) + . expect2xx + + stealPasswdResetToken :: TestEnv -> EmailAddress -> (MonadReader TestEnv m, MonadIO m) => m Text + stealPasswdResetToken env (toStrict . toByteString -> email) = do + resp <- + call . get $ + (env ^. teBrig) + . path "/i/users/password-reset-code" + . contentJson + . queryItem "email" email + . expect2xx + maybe (error "could not find and/or parse passwd reset token") pure $ + responseBody resp ^? _Just . key "code" . _String + + login :: TestEnv -> Aeson.Value -> (MonadReader TestEnv m, MonadIO m) => m () + login env loginBody = + void . call . post $ + (versioned "v13") + . (env ^. teBrig) + . path "/login" + . contentJson + . queryItem "persist" "true" + . body (RequestBodyLBS (Aeson.encode loginBody)) + . expect2xx + specImportToScimFromInvitation :: SpecWith TestEnv specImportToScimFromInvitation = describe "Create with TM invitation; then re-provision with SCIM" $ do @@ -1230,7 +1326,8 @@ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do runSpar $ BrigAccess.setHandle uid handle pure usr let memberIdWithSSO = userId memberWithSSO - externalId = either error id $ veidToText =<< Intra.veidFromBrigUser memberWithSSO Nothing Nothing + idpIssuer = idp ^. SAML.idpMetadata . SAML.edIssuer + externalId = either error id $ veidToText =<< Intra.newVeidFromBrigUser memberWithSSO (Just idpIssuer) -- NOTE: once SCIM is enabled, SSO auto-provisioning is disabled tok <- registerScimToken teamid (Just (idp ^. SAML.idpId)) @@ -2134,15 +2231,17 @@ specDeleteUser = do !!! const 405 === statusCode describe "DELETE /Users/:id" $ do it "should delete user from brig, spar.scim_user_times, spar.user" $ do - (tok, _) <- registerIdPAndScimToken + (tok, (_, _, idp)) <- registerIdPAndScimToken user <- randomScimUser storedUser <- createUser tok user let uid :: UserId = scimUserId storedUser uref :: SAML.UserRef <- do mUsr <- runSpar $ BrigAccess.getAccount Intra.WithPendingInvitations uid - let err = error . ("brig user without UserRef: " <>) . show - case (\usr -> Intra.veidFromBrigUser usr Nothing Nothing) <$> mUsr of - bad@(Just (Right veid)) -> runValidScimIdEither pure (const $ err bad) veid + let cond usr = Intra.newVeidFromBrigUser usr (Just (idp ^. SAML.idpMetadata . SAML.edIssuer)) + good bad = runValidScimIdEither pure (const $ err bad) + err bad = error $ "brig user without UserRef: " <> show (bad, user) + case cond <$> mUsr of + bad@(Just (Right veid)) -> good bad veid bad -> err bad spar <- view teSpar deleteUser_ (Just tok) (Just uid) spar diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index acfad1fe0a2..02fb3bae4e0 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -650,8 +650,8 @@ instance IsUser (WrappedScimUser SparTag) where maybeUserId = Nothing maybeHandle = Just (parseHandle . Scim.User.userName . fromWrappedScimUser) maybeName = Just (fmap Name . Scim.User.displayName . fromWrappedScimUser) - maybeTenant = Nothing - maybeSubject = Nothing + maybeTenant = Nothing -- we don't know from the scim schema. + maybeSubject = Nothing -- dito. maybeScimExternalId = Just $ Scim.User.externalId . fromWrappedScimUser maybeLocale = Just @@ -666,21 +666,12 @@ instance IsUser User where maybeUserId = Just userId maybeHandle = Just userHandle maybeName = Just (Just . userDisplayName) - maybeTenant = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing Nothing - & either - (const Nothing) - (fmap SAML._uidTenant . veidUref) - maybeSubject = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing Nothing - & either - (const Nothing) - (fmap SAML._uidSubject . veidUref) - maybeScimExternalId = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing Nothing - & either - (const Nothing) - (runValidScimIdEither Intra.urefToExternalId (Just . fromEmail)) + maybeTenant = Just $ (fmap SAML._uidTenant . veidUref) <=< Intra.oldVeidFromBrigUser + maybeSubject = Just $ (fmap SAML._uidSubject . veidUref) <=< Intra.oldVeidFromBrigUser + maybeScimExternalId = + Just $ + Intra.oldVeidFromBrigUser + >=> (runValidScimIdEither Intra.urefToExternalId (Just . fromEmail)) maybeLocale = Just $ Just . userLocale -- | For all properties that are present in both @u1@ and @u2@, check that they match. From e219f52b9e1f810e6d7af78e218f244c9c014c64 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 11 Dec 2025 11:28:46 +0100 Subject: [PATCH 19/60] [WPB-22287] fix saml xml headers (#4898) * Add `` to SAML/XML output. --- changelog.d/3-bug-fixes/WPB-22287-fix-saml-xml-headers | 1 + libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs | 2 +- libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs | 4 ++-- services/spar/test-integration/Test/Spar/APISpec.hs | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-22287-fix-saml-xml-headers diff --git a/changelog.d/3-bug-fixes/WPB-22287-fix-saml-xml-headers b/changelog.d/3-bug-fixes/WPB-22287-fix-saml-xml-headers new file mode 100644 index 00000000000..a030675108f --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-22287-fix-saml-xml-headers @@ -0,0 +1 @@ +Add `` to SAML/XML output. \ No newline at end of file diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs index 01b26ae093e..bfff13e20ad 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs @@ -83,7 +83,7 @@ defNameSpaces = encode :: forall a. (HasXMLRoot a) => a -> LT encode = Text.XML.renderText settings . renderToDocument where - settings = def {rsNamespaces = nameSpaces (Proxy @a), rsXMLDeclaration = False} + settings = def {rsNamespaces = nameSpaces (Proxy @a), rsXMLDeclaration = True} decode :: forall m a. (HasXMLRoot a, MonadError String m) => LT -> m a decode = either (throwError . show @SomeException) parseFromDocument . parseText def diff --git a/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs b/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs index ab886a44bfd..9b4ae65b546 100644 --- a/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs +++ b/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs @@ -102,7 +102,7 @@ spec = describe "API" $ do <> "
" method=\"post\" accept-charset=\"utf-8\">" <> " " value=\"PHNhbWxwOkxvZ291dFJlcXVlc3QgSUQ9ImQyYjdjMzg4Y2VjMzZmYTdjMzljMjhmZDI5ODY0NGE4IiBJc3N1ZUluc3RhbnQ9IjIwMDQtMDEtMjFUMTk6MDA6NDlaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPiAgICA8SXNzdWVyIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwczovL0lkZW50aXR5UHJvdmlkZXIuY29tL1NBTUw8L0lzc3Vlcj4gICAgPE5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnBlcnNpc3RlbnQiIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4wMDVhMDZlMC1hZDgyLTExMGQtYTU1Ni0wMDQwMDViMTNhMmI8L05hbWVJRD4gICAgPHNhbWxwOlNlc3Npb25JbmRleD4xPC9zYW1scDpTZXNzaW9uSW5kZXg+PC9zYW1scDpMb2dvdXRSZXF1ZXN0Pg==\"/>" + <> " value=\"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbHA6TG9nb3V0UmVxdWVzdCBJRD0iZDJiN2MzODhjZWMzNmZhN2MzOWMyOGZkMjk4NjQ0YTgiIElzc3VlSW5zdGFudD0iMjAwNC0wMS0yMVQxOTowMDo0OVoiIFZlcnNpb249IjIuMCIgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+ICAgIDxJc3N1ZXIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHBzOi8vSWRlbnRpdHlQcm92aWRlci5jb20vU0FNTDwvSXNzdWVyPiAgICA8TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6cGVyc2lzdGVudCIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjAwNWEwNmUwLWFkODItMTEwZC1hNTU2LTAwNDAwNWIxM2EyYjwvTmFtZUlEPiAgICA8c2FtbHA6U2Vzc2lvbkluZGV4PjE8L3NhbWxwOlNlc3Npb25JbmRleD48L3NhbWxwOkxvZ291dFJlcXVlc3Q+\"/>" <> "" @@ -111,7 +111,7 @@ spec = describe "API" $ do <> "" Right (SomeSAMLRequest -> doc) = XML.parseText XML.def have spuri = [uri|https://ServiceProvider.com/SAML/SLO/Browser/%%|] - Right want `shouldBe` (fmapL show . parseText def . cs $ mimeRender (Proxy @HTML) (FormRedirect spuri doc)) + (fmapL show . parseText def . cs $ mimeRender (Proxy @HTML) (FormRedirect spuri doc)) `shouldBe` Right want describe "simpleVerifyAuthnResponse" $ do let check :: Bool -> Maybe Bool -> Bool -> Spec diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 7ea165fcb95..0212cbd97a1 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -929,7 +929,7 @@ specCRUDIdentityProvider = do rawmeta <- call $ callIdpGetRaw (env ^. teSpar) (Just owner) (idp ^. idpId) liftIO $ do idp `shouldBe` idp' - let prefix = " Date: Thu, 11 Dec 2025 14:20:18 +0100 Subject: [PATCH 20/60] Optimize more Postgresql queries for getting converstaion members (#4901) --- .../3-bug-fixes/optimize-conv-member-queries | 2 +- .../src/Wire/ConversationStore/Postgres.hs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/changelog.d/3-bug-fixes/optimize-conv-member-queries b/changelog.d/3-bug-fixes/optimize-conv-member-queries index 329c12ab249..8125a7bbdd3 100644 --- a/changelog.d/3-bug-fixes/optimize-conv-member-queries +++ b/changelog.d/3-bug-fixes/optimize-conv-member-queries @@ -1 +1 @@ -Optimize Postgresql queries for getting conversation members \ No newline at end of file +Optimize Postgresql queries for getting conversation members (#4896, ##) \ No newline at end of file diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs index f14ddb30b74..745c5255e1e 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs @@ -272,7 +272,13 @@ getConversationsImpl cids = do (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) FROM conversation_member WHERE conv = ANY ($1 :: uuid[]) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ANY ($1 :: uuid[])) + + UNION ALL + + SELECT (conv :: uuid), ("user" :: uuid), (service :: uuid?), (provider :: uuid?), (otr_muted_status :: integer?), (otr_muted_ref :: text?), + (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) + FROM conversation_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ANY ($1 :: uuid[])) |] selectAllRemoteMembers :: Hasql.Statement [ConvId] [RemoteMemberRow] selectAllRemoteMembers = @@ -280,7 +286,12 @@ getConversationsImpl cids = do [vectorStatement|SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) FROM local_conversation_remote_member WHERE conv = ANY ($1 :: uuid[]) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ANY ($1 :: uuid[])) + + UNION ALL + + SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) + FROM local_conversation_remote_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ANY ($1 :: uuid[])) |] findMembers :: (HasField "id_" a b, Eq b) => ConvId -> Maybe ConvId -> [(ConvId, a)] -> [a] From c0ed4b10e9fbf561135250e567b9e880e59926fb Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 11 Dec 2025 14:40:46 +0100 Subject: [PATCH 21/60] Allow configuring page size and parallelism for conversation migration to PostgreSQL (#4904) --- changelog.d/2-features/faster-migration | 10 ++++ .../templates/configmap.yaml | 2 + charts/background-worker/values.yaml | 3 + .../src/Wire/ConversationStore/Migration.hs | 57 ++++++++++++------- .../background-worker.integration.yaml | 3 + .../src/Wire/BackgroundWorker.hs | 2 +- .../src/Wire/BackgroundWorker/Options.hs | 4 +- .../src/Wire/MigrateConversations.hs | 8 +-- 8 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 changelog.d/2-features/faster-migration diff --git a/changelog.d/2-features/faster-migration b/changelog.d/2-features/faster-migration new file mode 100644 index 00000000000..412c24b5920 --- /dev/null +++ b/changelog.d/2-features/faster-migration @@ -0,0 +1,10 @@ +Allow configuring page size and parallelism for conversation migration to +PostgreSQL. This can be configured like this: + +```yaml +background-worker: + config: + migrateConversationsOptions: + pageSize: 10000 + parallelism: 2 +``` diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index a9a1f88e4bc..81e78f5ca77 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -90,6 +90,8 @@ data: {{- end }} migrateConversations: {{ .migrateConversations }} + migrateConversationsOptions: +{{toYaml .migrateConversationsOptions | indent 6 }} backendNotificationPusher: {{toYaml .backendNotificationPusher | indent 6 }} diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml index 736e4983eb7..57f3ce00706 100644 --- a/charts/background-worker/values.yaml +++ b/charts/background-worker/values.yaml @@ -66,6 +66,9 @@ config: # `settings.postgresMigration.conversation` with `migration-to-postgresql` # before setting this to `true`. migrateConversations: false + migrateConversationsOptions: + pageSize: 10000 + parallelism: 2 backendNotificationPusher: pushBackoffMinWait: 10000 # in microseconds, so 10ms diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs index 295910399f6..6cae2fc9cf6 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs @@ -22,6 +22,7 @@ module Wire.ConversationStore.Migration where import Cassandra import Cassandra.Settings hiding (pageSize) import Control.Error (lastMay) +import Data.Aeson (FromJSON) import Data.Conduit import Data.Conduit.Internal (zipSources) import Data.Conduit.List qualified as C @@ -36,6 +37,7 @@ import Data.Time.Calendar.OrdinalDate (fromOrdinalDate) import Data.Tuple.Extra import Data.Vector (Vector) import Data.Vector qualified as Vector +import GHC.Generics (Generically (..)) import Hasql.Pool qualified as Hasql import Hasql.Statement qualified as Hasql import Hasql.TH @@ -71,6 +73,8 @@ import Wire.ConversationStore.Migration.Cleanup import Wire.ConversationStore.Migration.Types import Wire.ConversationStore.MigrationLock import Wire.Postgres +import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (..), unsafePooledMapConcurrentlyN_) +import Wire.Sem.Concurrency.IO (unsafelyPerformConcurrency) import Wire.Sem.Logger (mapLogger) import Wire.Sem.Logger.TinyLog (loggerToTinyLog) import Wire.Sem.Paging.Cassandra @@ -79,15 +83,22 @@ import Wire.Util -- * Top level logic -type EffectStack = [State Int, Input ClientState, Input Hasql.Pool, Async, Race, TinyLog, Embed IO, Final IO] +type EffectStack = [State Int, Input ClientState, Input Hasql.Pool, Async, Race, TinyLog, Embed IO, Concurrency 'Unsafe, Final IO] -migrateConvsLoop :: ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO () -migrateConvsLoop cassClient pgPool logger migCounter migFinished migFailed = - migrationLoop cassClient pgPool logger "conversations" migFinished migFailed $ migrateAllConversations migCounter +data MigrationOptions = MigrationOptions + { pageSize :: Int32, + parallelism :: Int + } + deriving (Show, Eq, Generic) + deriving (FromJSON) via Generically MigrationOptions -migrateUsersLoop :: ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO () -migrateUsersLoop cassClient pgPool logger migCounter migFinished migFailed = - migrationLoop cassClient pgPool logger "users" migFinished migFailed $ migrateAllUsers migCounter +migrateConvsLoop :: MigrationOptions -> ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO () +migrateConvsLoop migOpts cassClient pgPool logger migCounter migFinished migFailed = + migrationLoop cassClient pgPool logger "conversations" migFinished migFailed $ migrateAllConversations migOpts migCounter + +migrateUsersLoop :: MigrationOptions -> ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO () +migrateUsersLoop migOpts cassClient pgPool logger migCounter migFinished migFailed = + migrationLoop cassClient pgPool logger "users" migFinished migFailed $ migrateAllUsers migOpts migCounter migrationLoop :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Prometheus.Counter -> Prometheus.Counter -> ConduitT () Void (Sem EffectStack) () -> IO () migrationLoop cassClient pgPool logger name migFinished migFailed migration = do @@ -128,6 +139,7 @@ migrationLoop cassClient pgPool logger name migFinished migFailed migration = do interpreter :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Sem EffectStack a -> IO (Int, a) interpreter cassClient pgPool logger name = runFinal + . unsafelyPerformConcurrency . embedToFinal . loggerToTinyLog logger . mapLogger (Log.field "migration" name .) @@ -138,9 +150,6 @@ interpreter cassClient pgPool logger name = . runInputConst cassClient . runState 0 -pageSize :: Int32 -pageSize = 10000 - migrateAllConversations :: ( Member (Input Hasql.Pool) r, Member (Embed IO) r, @@ -148,15 +157,17 @@ migrateAllConversations :: Member TinyLog r, Member Async r, Member Race r, - Member (State Int) r + Member (State Int) r, + Member (Concurrency Unsafe) r ) => + MigrationOptions -> Prometheus.Counter -> ConduitM () Void (Sem r) () -migrateAllConversations migCounter = do +migrateAllConversations migOpts migCounter = do lift $ info $ Log.msg (Log.val "migrateAllConversations") - withCount (paginateSem select (paramsP LocalQuorum () pageSize) x5) - .| logRetrievedPage - .| C.mapM_ (mapM_ (handleErrors (migrateConversation migCounter) "conv")) + withCount (paginateSem select (paramsP LocalQuorum () migOpts.pageSize) x5) + .| logRetrievedPage migOpts.pageSize + .| C.mapM_ (unsafePooledMapConcurrentlyN_ migOpts.parallelism (handleErrors (migrateConversation migCounter) "conv")) where select :: PrepQuery R () (Identity ConvId) select = "select conv from conversation" @@ -168,21 +179,23 @@ migrateAllUsers :: Member TinyLog r, Member Async r, Member Race r, - Member (State Int) r + Member (State Int) r, + Member (Concurrency 'Unsafe) r ) => + MigrationOptions -> Prometheus.Counter -> ConduitM () Void (Sem r) () -migrateAllUsers migCounter = do +migrateAllUsers migOpts migCounter = do lift $ info $ Log.msg (Log.val "migrateAllUsers") - withCount (paginateSem select (paramsP LocalQuorum () pageSize) x5) - .| logRetrievedPage - .| C.mapM_ (mapM_ (handleErrors (migrateUser migCounter) "user")) + withCount (paginateSem select (paramsP LocalQuorum () migOpts.pageSize) x5) + .| logRetrievedPage migOpts.pageSize + .| C.mapM_ (unsafePooledMapConcurrentlyN_ migOpts.parallelism (handleErrors (migrateUser migCounter) "user")) where select :: PrepQuery R () (Identity UserId) select = "select distinct user from user_remote_conv" -logRetrievedPage :: (Member TinyLog r) => ConduitM (Int32, [Identity (Id a)]) [Id a] (Sem r) () -logRetrievedPage = +logRetrievedPage :: (Member TinyLog r) => Int32 -> ConduitM (Int32, [Identity (Id a)]) [Id a] (Sem r) () +logRetrievedPage pageSize = C.mapM ( \(i, rows) -> do let estimatedRowsSoFar = (i - 1) * pageSize + fromIntegral (length rows) diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index ecb27c4c449..4ee7abbe100 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -65,6 +65,9 @@ backendNotificationPusher: remotesRefreshInterval: 10000 # 10ms migrateConversations: false +migrateConversationsOptions: + pageSize: 10000 + parallelism: 2 # Background jobs consumer configuration for integration backgroundJobs: diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index 657263eb654..c30c1d809aa 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -53,7 +53,7 @@ run opts = do then runAppT env $ withNamedLogger "migrate-conversations" $ - MigrateConversations.startWorker + MigrateConversations.startWorker opts.migrateConversationsOptions else pure $ pure () cleanupJobs <- runAppT env $ diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index 899f0904adc..48cc531b586 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -28,6 +28,7 @@ import Network.AMQP.Extended import System.Logger.Extended import Util.Options import Wire.ConversationStore (PostgresMigrationOpts) +import Wire.ConversationStore.Migration (MigrationOptions) data Opts = Opts { logLevel :: !Level, @@ -49,7 +50,8 @@ data Opts = Opts postgresqlPassword :: !(Maybe FilePathSecrets), postgresqlPool :: !PoolConfig, postgresMigration :: !PostgresMigrationOpts, - migrateConversations :: Bool, + migrateConversations :: !Bool, + migrateConversationsOptions :: !MigrationOptions, backgroundJobs :: BackgroundJobsConfig, federationDomain :: Domain } diff --git a/services/background-worker/src/Wire/MigrateConversations.hs b/services/background-worker/src/Wire/MigrateConversations.hs index 9e6503899e2..75587e1ae5b 100644 --- a/services/background-worker/src/Wire/MigrateConversations.hs +++ b/services/background-worker/src/Wire/MigrateConversations.hs @@ -25,8 +25,8 @@ import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Util import Wire.ConversationStore.Migration -startWorker :: AppT IO CleanupAction -startWorker = do +startWorker :: MigrationOptions -> AppT IO CleanupAction +startWorker migOpts = do cassClient <- asks (.cassandraGalley) pgPool <- asks (.hasqlPool) logger <- asks (.logger) @@ -39,8 +39,8 @@ startWorker = do userMigFinished <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migration_finished" "Whether the migration of remote conversation membership data to Postgresql is finished successfully" userMigFailed <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migration_failed" "Whether the migration of remote conversation membership data to Postgresql has failed" - convLoop <- async . lift $ migrateConvsLoop cassClient pgPool logger convMigCounter convMigFinished convMigFailed - userLoop <- async . lift $ migrateUsersLoop cassClient pgPool logger userMigCounter userMigFinished userMigFailed + convLoop <- async . lift $ migrateConvsLoop migOpts cassClient pgPool logger convMigCounter convMigFinished convMigFailed + userLoop <- async . lift $ migrateUsersLoop migOpts cassClient pgPool logger userMigCounter userMigFinished userMigFailed Log.info logger $ Log.msg (Log.val "started conversation migration") pure $ do From 142747b5ddb95696f931397a489c7658934ab690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20L=C3=A4ll?= Date: Thu, 11 Dec 2025 15:53:57 +0200 Subject: [PATCH 22/60] Add missing path to helm (#4902) * Add missing path to helm * Improve captured HTTP path name --- charts/nginz/values.yaml | 3 +++ libs/wire-api/src/Wire/API/Routes/Public/Brig.hs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index d902a367a5b..d7f3b9c482f 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -438,6 +438,9 @@ nginx_conf: - path: /teams/([^/]*)/apps$ envs: - all + - path: /teams/([^/]*)/apps/([^/]*)$ + envs: + - all - path: /teams/([^/]*)/apps/([^/]*)/cookies$ envs: - all diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index f1207b00b13..e947cfa2077 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -2125,7 +2125,7 @@ type AppsAPI = :> "teams" :> Capture "tid" TeamId :> "apps" - :> Capture "id" UserId + :> Capture "uid" UserId :> Get '[JSON] GetApp ) :<|> Named From 8663a561235a5f3953477bcc0d259fe976d6579e Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 11 Dec 2025 14:57:25 +0100 Subject: [PATCH 23/60] galley-integration: Deflake test (#4900) --- services/galley/test/integration/API.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index fe8430d0018..30a00c15d7a 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1515,7 +1515,7 @@ getConvsOk2 = do Just actual -> do assertEqual "name mismatch" expected.metadata.cnvmName actual.cnvMetadata.cnvmName assertEqual "members.self" expected.members.self (Just actual.cnvMembers.cmSelf) - assertEqual "members.others" expected.members.others actual.cnvMembers.cmOthers + assertEqual "members.others" (sort expected.members.others) (sort actual.cnvMembers.cmOthers) getConvsFailMaxSizeV2 :: TestM () getConvsFailMaxSizeV2 = do From dc4a89119dd5e1df95b35c17b31f4d1097cbdcef Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 12 Dec 2025 10:22:39 +0100 Subject: [PATCH 24/60] WPB-22101: fix SCIM groups endpoint to only return SCIM-managed groups, not wire-managed groups (#4906) --- changelog.d/3-bug-fixes/WPB-22101.md | 1 + integration/test/Test/Spar.hs | 49 +++++++++++++++++++ .../src/Wire/API/Routes/Internal/Brig.hs | 1 + .../wire-subsystems/src/Wire/BrigAPIAccess.hs | 2 +- .../src/Wire/BrigAPIAccess/Rpc.hs | 8 +-- .../src/Wire/ScimSubsystem/Interpreter.hs | 2 +- .../src/Wire/UserGroupStore.hs | 1 + .../src/Wire/UserGroupStore/Postgres.hs | 8 ++- .../src/Wire/UserGroupSubsystem.hs | 2 +- .../Wire/UserGroupSubsystem/Interpreter.hs | 8 ++- .../Wire/MockInterpreters/UserGroupStore.hs | 5 ++ services/brig/src/Brig/API/Internal.hs | 5 +- 12 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-22101.md diff --git a/changelog.d/3-bug-fixes/WPB-22101.md b/changelog.d/3-bug-fixes/WPB-22101.md new file mode 100644 index 00000000000..bba88783a70 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-22101.md @@ -0,0 +1 @@ +Fix SCIM groups endpoint to only return SCIM-managed groups, not wire-managed groups diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index 5fc7664868c..b6df2edae78 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -639,6 +639,55 @@ testSparScimDeleteUserGroup = do getScimUserGroup OwnDomain tok gid `bindResponse` \resp -> do resp.status `shouldMatchInt` 404 +testSparScimGroupSearchOnlyReturnsScimGroups :: (HasCallStack) => App () +testSparScimGroupSearchOnlyReturnsScimGroups = do + (owner, tid, [regularMember]) <- createTeam OwnDomain 2 + tok <- createScimTokenV6 owner def >>= \resp -> resp.json %. "token" >>= asString + + assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" "disabled" + assertSuccess =<< setTeamFeatureStatus owner tid "sso" "enabled" + void $ registerTestIdPWithMetaWithPrivateCreds owner + + let mkScimMemberCandidate :: App String + mkScimMemberCandidate = do + scimUserEmail <- randomEmail + scimUser <- randomScimUserWith def {mkExternalId = pure scimUserEmail} + uid <- createScimUser owner tok scimUser >>= getJSON 201 >>= (%. "id") >>= asString + registerInvitedUser OwnDomain tid scimUserEmail + pure uid + + scimUserId <- mkScimMemberCandidate + + -- Create a wire-managed group using the regular team member + regularMemberId <- regularMember %. "id" >>= asString + let wireGroupPayload = + object + [ "name" .= "wire-managed-group", + "members" .= [regularMemberId] + ] + wireGroupResp <- createUserGroup owner wireGroupPayload + wireGroupResp.status `shouldMatchInt` 200 + wireGroupId <- wireGroupResp.json %. "id" >>= asString + + -- Verify the wire-managed group was created with managedBy = "wire" + wireGroupGet <- getUserGroup owner wireGroupId + wireGroupGet.status `shouldMatchInt` 200 + wireGroupGet.json %. "managedBy" `shouldMatch` "wire" + + -- Create a SCIM-managed group using the SCIM user + scimGroupResp <- createScimUserGroup OwnDomain tok $ mkScimGroup "scim-managed-group" [mkScimUser scimUserId] + scimGroupResp.status `shouldMatchInt` 201 + scimGroupId <- scimGroupResp.json %. "id" >>= asString + + -- Call the SCIM groups search endpoint (without filter) + filterScimUserGroup OwnDomain tok Nothing `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resources <- resp.json %. "Resources" >>= asList + resourceIds <- for resources $ \g -> g %. "id" >>= asString + + -- Assert: Only the SCIM-managed group should be returned, not the wire-managed group + resourceIds `shouldMatch` [scimGroupId] + ---------------------------------------------------------------------- -- saml stuff diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index 36bb424f2fb..3e69b5c8778 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -283,6 +283,7 @@ type GetGroupsInternal = :> "user-groups" :> Capture "tid" TeamId :> QueryParam' [Optional, Strict] "nameContains" Text.Text + :> QueryParam' [Optional, Strict] "managedBy" ManagedBy :> Get '[Servant.JSON] UserGroupPageWithMembers ) diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs index c9f9aa41688..4feb73b6a23 100644 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs @@ -161,7 +161,7 @@ data BrigAPIAccess m a where GetAccountsBy :: GetBy -> BrigAPIAccess m [User] CreateGroupInternal :: ManagedBy -> TeamId -> Maybe UserId -> NewUserGroup -> BrigAPIAccess m (Either Wai.Error UserGroup) GetGroupInternal :: TeamId -> UserGroupId -> Bool -> BrigAPIAccess m (Maybe UserGroup) - GetGroupsInternal :: TeamId -> Maybe Scim.Filter -> BrigAPIAccess m UserGroupPageWithMembers + GetGroupsInternal :: TeamId -> Maybe Scim.Filter -> Maybe ManagedBy -> BrigAPIAccess m UserGroupPageWithMembers UpdateGroup :: UpdateGroupInternalRequest -> BrigAPIAccess m (Either Wai.Error ()) DeleteGroupInternal :: ManagedBy -> TeamId -> UserGroupId -> BrigAPIAccess m (Either DeleteGroupManagedError ()) diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs index 08df77e2173..541e3badf6e 100644 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs @@ -124,8 +124,8 @@ interpretBrigAccess brigEndpoint = getAccountsBy localGetBy CreateGroupInternal managedBy teamId creatorUserId newGroup -> createGroupInternal managedBy teamId creatorUserId newGroup - GetGroupsInternal tid mbFilter -> - getGroupsInternal tid mbFilter + GetGroupsInternal tid mbFilter mbManagedBy -> + getGroupsInternal tid mbFilter mbManagedBy GetGroupInternal tid gid includeChannels -> getGroupInternal tid gid includeChannels UpdateGroup req -> @@ -605,8 +605,9 @@ getGroupsInternal :: (Member Rpc r, Member (Input Endpoint) r, Member (Error ParseException) r) => TeamId -> Maybe Scim.Filter -> + Maybe ManagedBy -> Sem r UserGroupPageWithMembers -getGroupsInternal tid mbFilter = do +getGroupsInternal tid mbFilter mbManagedBy = do maybeDisplayName :: Maybe Text <- case mbFilter of Just filter' -> case filter' of FilterAttrCompare (AttrPath _schema "displayName" Nothing) OpCo (ValString str) -> pure $ Just str @@ -617,6 +618,7 @@ getGroupsInternal tid mbFilter = do method GET . paths ["i", "user-groups", toByteString' tid] . maybe id (queryItem "nameContains" . Text.encodeUtf8) maybeDisplayName + . maybe id (queryItem "managedBy" . toByteString') mbManagedBy . expect2xx decodeBodyOrThrow "brig" r diff --git a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs index fcac6f81468..b946e189ed0 100644 --- a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs @@ -132,7 +132,7 @@ scimGetUserGroupsImpl :: Maybe Scim.Filter -> Sem r (Scim.ListResponse (SCG.StoredGroup SparTag)) scimGetUserGroupsImpl tid mbFilter = do - UserGroupPage {page} :: UserGroupPageWithMembers <- BrigAPI.getGroupsInternal tid mbFilter + UserGroupPage {page} :: UserGroupPageWithMembers <- BrigAPI.getGroupsInternal tid mbFilter (Just ManagedByScim) ScimSubsystemConfig scimBaseUri <- input pure . Scim.fromList $ toStoredGroup scimBaseUri <$> page diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore.hs b/libs/wire-subsystems/src/Wire/UserGroupStore.hs index cb4c9449046..c8ccbbfae34 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore.hs @@ -34,6 +34,7 @@ import Wire.PaginationState data UserGroupPageRequest = UserGroupPageRequest { team :: TeamId, searchString :: Maybe Text, + managedByFilter :: Maybe ManagedBy, paginationState :: PaginationState UserGroupId, sortOrder :: SortOrder, pageSize :: PageSize, diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs index c5609389a38..ad6b218987b 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs @@ -280,9 +280,15 @@ getUserGroupsWithMembers req = groupMatchIdName :: UserGroupPageRequest -> [QueryFragment] groupMatchIdName req = clause1 "ug.team_id" "=" req.team - : case req.searchString of + : managedByClause + <> nameClause + where + nameClause = case req.searchString of Just name -> [like "ug.name" name] Nothing -> [] + managedByClause = case req.managedByFilter of + Just managedBy -> [clause1 "ug.managed_by" "=" (managedByToInt32 managedBy)] + Nothing -> [] groupPaginationWhereClause :: UserGroupPageRequest -> [QueryFragment] groupPaginationWhereClause req = case paginationClause req.paginationState of diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs index 68071e5d671..a674b7fb6a2 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs @@ -75,7 +75,7 @@ data UserGroupSubsystem m a where -- Internal API handlers CreateGroupInternal :: ManagedBy -> TeamId -> Maybe UserId -> NewUserGroup -> UserGroupSubsystem r UserGroup GetGroupInternal :: TeamId -> UserGroupId -> Bool -> UserGroupSubsystem m (Maybe UserGroup) - GetGroupsInternal :: TeamId -> Maybe Text -> UserGroupSubsystem m UserGroupPageWithMembers + GetGroupsInternal :: TeamId -> Maybe Text -> Maybe ManagedBy -> UserGroupSubsystem m UserGroupPageWithMembers ResetUserGroupInternal :: UpdateGroupInternalRequest -> UserGroupSubsystem m () makeSem ''UserGroupSubsystem diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs index ff446ae855a..527bb3fc084 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs @@ -86,7 +86,7 @@ interpretUserGroupSubsystem = interpret $ \case -- Internal API handlers CreateGroupInternal managedBy team mbCreator newGroup -> createUserGroupFullImpl managedBy team mbCreator newGroup GetGroupInternal tid gid includeChannels -> getUserGroupInternal tid gid includeChannels - GetGroupsInternal tid displayNameSubstring -> getUserGroupsInternal tid displayNameSubstring + GetGroupsInternal tid displayNameSubstring mbManagedBy -> getUserGroupsInternal tid displayNameSubstring mbManagedBy ResetUserGroupInternal req -> resetUserGroupInternal req data UserGroupSubsystemError @@ -268,6 +268,7 @@ getUserGroups getter search = do search.lastId, team = team, searchString = search.query, + managedByFilter = Nothing, includeMemberCount = search.includeMemberCount, includeChannels = search.includeChannels } @@ -282,8 +283,9 @@ getUserGroupsInternal :: ) => TeamId -> Maybe Text -> + Maybe ManagedBy -> Sem r UserGroupPageWithMembers -getUserGroupsInternal team displayNameSubstring = do +getUserGroupsInternal team displayNameSubstring mbManagedBy = do let -- hscim doesn't support pagination at the time of writing this, -- so we better fit all groups into one page! pageSize = pageSizeFromIntUnsafe 500 @@ -294,6 +296,7 @@ getUserGroupsInternal team displayNameSubstring = do paginationState = mkPaginationState SortByName (Just "displayName") Nothing Nothing, team = team, searchString = displayNameSubstring, + managedByFilter = mbManagedBy, includeMemberCount = True, includeChannels = False } @@ -535,6 +538,7 @@ removeUserFromAllGroups uid tid = do fmap Store.userGroupCreatedAtPaginationState mug, team = tid, searchString = Nothing, + managedByFilter = Nothing, includeMemberCount = False, includeChannels = False } diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs index 0e01041ab80..a7cbce4b3f6 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs @@ -142,6 +142,7 @@ getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do dropBeforeStart, orderByKeys, narrowToSearchString, + narrowToManagedBy, narrowToTeam :: [((TeamId, UserGroupId), UserGroup)] -> [((TeamId, UserGroupId), UserGroup)] @@ -150,10 +151,14 @@ getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do . dropBeforeStart . orderByKeys . narrowToSearchString + . narrowToManagedBy . narrowToTeam narrowToTeam = filter (\((thisTid, _), _) -> thisTid == team) + narrowToManagedBy = + filter (\(_, ug) -> maybe True (== ug.managedBy) managedByFilter) + narrowToSearchString = filter (\(_, ug) -> maybe True (`T.isInfixOf` userGroupNameToText ug.name) searchString) diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index e38db0c168d..c7b555cf10e 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -1030,9 +1030,10 @@ getGroupsInternalH :: ) => TeamId -> Maybe T.Text -> + Maybe ManagedBy -> Handler r UserGroupPageWithMembers -getGroupsInternalH tid nameContains = - lift . liftSem $ getGroupsInternal tid nameContains +getGroupsInternalH tid nameContains managedBy = + lift . liftSem $ getGroupsInternal tid nameContains managedBy updateGroupInternalH :: ( Member UserGroupSubsystem r From bf3e22e7f9e40dca703e697957021e41b2293760 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 12 Dec 2025 13:07:45 +0100 Subject: [PATCH 25/60] List active users that don't support MLS (#4888) * Add mls-users skeleton * Find active users * Remove unneeded galley cassandra use * Fix user query * Add CHANGELOG entry * Regenerate haskell packages * Fix warnings * Ormolu * Fix year * Fix logic in getUserResult * Rename included to matches --- cabal.project | 1 + changelog.d/5-internal/active-users-no-mls | 1 + nix/local-haskell-packages.nix | 1 + tools/db/mls-users/.ormolu | 1 + tools/db/mls-users/README.md | 3 + tools/db/mls-users/app/Main.hs | 23 ++++ tools/db/mls-users/default.nix | 52 +++++++++ tools/db/mls-users/mls-users.cabal | 99 ++++++++++++++++ tools/db/mls-users/src/MlsUsers/Lib.hs | 124 +++++++++++++++++++++ tools/db/mls-users/src/MlsUsers/Types.hs | 123 ++++++++++++++++++++ 10 files changed, 428 insertions(+) create mode 100644 changelog.d/5-internal/active-users-no-mls create mode 120000 tools/db/mls-users/.ormolu create mode 100644 tools/db/mls-users/README.md create mode 100644 tools/db/mls-users/app/Main.hs create mode 100644 tools/db/mls-users/default.nix create mode 100644 tools/db/mls-users/mls-users.cabal create mode 100644 tools/db/mls-users/src/MlsUsers/Lib.hs create mode 100644 tools/db/mls-users/src/MlsUsers/Types.hs diff --git a/cabal.project b/cabal.project index e4434bfad3a..80f34bf09e3 100644 --- a/cabal.project +++ b/cabal.project @@ -49,6 +49,7 @@ packages: , tools/db/inconsistencies/ , tools/db/migrate-sso-feature-flag/ , tools/db/migrate-features/ + , tools/db/mls-users/ , tools/db/move-team/ , tools/db/phone-users/ , tools/db/repair-handles/ diff --git a/changelog.d/5-internal/active-users-no-mls b/changelog.d/5-internal/active-users-no-mls new file mode 100644 index 00000000000..fa33cb7a9fe --- /dev/null +++ b/changelog.d/5-internal/active-users-no-mls @@ -0,0 +1 @@ +Add `mls-users` tool to list all active users that don't support MLS. diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 00e5d82aaec..2e065b6d403 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -50,6 +50,7 @@ inconsistencies = hself.callPackage ../tools/db/inconsistencies/default.nix { inherit gitignoreSource; }; migrate-features = hself.callPackage ../tools/db/migrate-features/default.nix { inherit gitignoreSource; }; migrate-sso-feature-flag = hself.callPackage ../tools/db/migrate-sso-feature-flag/default.nix { inherit gitignoreSource; }; + mls-users = hself.callPackage ../tools/db/mls-users/default.nix { inherit gitignoreSource; }; move-team = hself.callPackage ../tools/db/move-team/default.nix { inherit gitignoreSource; }; phone-users = hself.callPackage ../tools/db/phone-users/default.nix { inherit gitignoreSource; }; repair-brig-clients-table = hself.callPackage ../tools/db/repair-brig-clients-table/default.nix { inherit gitignoreSource; }; diff --git a/tools/db/mls-users/.ormolu b/tools/db/mls-users/.ormolu new file mode 120000 index 00000000000..ffc2ca9745e --- /dev/null +++ b/tools/db/mls-users/.ormolu @@ -0,0 +1 @@ +../../../.ormolu \ No newline at end of file diff --git a/tools/db/mls-users/README.md b/tools/db/mls-users/README.md new file mode 100644 index 00000000000..2da2fbeec6e --- /dev/null +++ b/tools/db/mls-users/README.md @@ -0,0 +1,3 @@ +# MLS users + +This program scans brig's users table and finds active users that don't support MLS. diff --git a/tools/db/mls-users/app/Main.hs b/tools/db/mls-users/app/Main.hs new file mode 100644 index 00000000000..18769e51240 --- /dev/null +++ b/tools/db/mls-users/app/Main.hs @@ -0,0 +1,23 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 Main where + +import qualified MlsUsers.Lib as Lib + +main :: IO () +main = Lib.main diff --git a/tools/db/mls-users/default.nix b/tools/db/mls-users/default.nix new file mode 100644 index 00000000000..0f1c8695642 --- /dev/null +++ b/tools/db/mls-users/default.nix @@ -0,0 +1,52 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, aeson +, aeson-pretty +, base +, bytestring +, cassandra-util +, conduit +, containers +, cql +, extra +, gitignoreSource +, imports +, lens +, lib +, optparse-applicative +, time +, tinylog +, types-common +, wire-api +}: +mkDerivation { + pname = "mls-users"; + version = "1.0.0"; + src = gitignoreSource ./.; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson + aeson-pretty + bytestring + cassandra-util + conduit + containers + cql + extra + imports + lens + optparse-applicative + time + tinylog + types-common + wire-api + ]; + executableHaskellDepends = [ base ]; + description = "Find users without MLS support"; + license = lib.licenses.agpl3Only; + mainProgram = "mls-users"; +} diff --git a/tools/db/mls-users/mls-users.cabal b/tools/db/mls-users/mls-users.cabal new file mode 100644 index 00000000000..1ee86961787 --- /dev/null +++ b/tools/db/mls-users/mls-users.cabal @@ -0,0 +1,99 @@ +cabal-version: 3.0 +name: mls-users +version: 1.0.0 +synopsis: Find users without MLS support +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2025 Wire Swiss GmbH +license: AGPL-3.0-only +build-type: Simple + +library + hs-source-dirs: src + default-language: Haskell2010 + exposed-modules: + MlsUsers.Lib + MlsUsers.Types + + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints -Wunused-packages + + build-depends: + , aeson + , aeson-pretty + , bytestring + , cassandra-util + , conduit + , containers + , cql + , extra + , imports + , lens + , optparse-applicative + , time + , tinylog + , types-common + , wire-api + + default-extensions: + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedLabels + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + RecordWildCards + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + +executable mls-users + main-is: Main.hs + build-depends: + , base + , mls-users + + hs-source-dirs: app + default-language: Haskell2010 + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints -Wunused-packages diff --git a/tools/db/mls-users/src/MlsUsers/Lib.hs b/tools/db/mls-users/src/MlsUsers/Lib.hs new file mode 100644 index 00000000000..9b23af9090d --- /dev/null +++ b/tools/db/mls-users/src/MlsUsers/Lib.hs @@ -0,0 +1,124 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 MlsUsers.Lib where + +import Cassandra as C hiding (All) +import Cassandra.Settings as C hiding (All) +import Conduit +import Control.Monad.Extra +import qualified Data.Conduit.Combinators as Conduit +import qualified Data.Conduit.List as ConduitL +import Data.Id +import qualified Data.Set as Set +import Data.Time.Clock +import qualified Database.CQL.Protocol as CQL +import Imports +import MlsUsers.Types +import Options.Applicative +import qualified System.Logger as Log +import System.Logger.Message ((.=), (~~)) +import Wire.API.User + +getUserResult :: Log.Logger -> ClientState -> UserRow -> IO Result +getUserResult logger brigClient ur = do + matches <- + andM + [ pure ur.activated, + pure $ ur.status == Just Active, + pure $ Set.notMember BaseProtocolMLSTag ur.supportedProtocols, + -- check that the user has at least one active client + do + now <- getCurrentTime + tms <- catMaybes <$> lookupClientsLastActiveTimestamps brigClient ur.userId + let active = any (\tm -> diffUTCTime now tm < 90 * nominalDay) tms + when active + $ Log.info logger + $ "user_record" + .= show ur + ~~ "last_active_timestamps" .= show tms + ~~ Log.msg (Log.val "active user found") + pure active + ] + + pure + Result + { totalUsers = 1, + activeNoMLS = if matches then 1 else 0 + } + +process :: Log.Logger -> Maybe Int -> ClientState -> IO Result +process logger limit brigClient = + runConduit + $ readUsers brigClient + .| Conduit.concat + .| (maybe (mapC id) takeC limit) + -- process users in chunks, yield a Result for each chunk + .| forever + ( ConduitL.isolate 10000 + .| (foldMapMC (getUserResult logger brigClient) >>= yield) + ) + .| Conduit.takeWhile ((> 0) . totalUsers) + -- join all results and log + .| ConduitL.scan (<>) mempty + `fuseUpstream` Conduit.mapM_ (\r -> Log.info logger $ "intermediate_result" .= show r) + +main :: IO () +main = do + opts <- execParser (info (helper <*> optsParser) desc) + logger <- initLogger + brigClient <- initCas opts.brigDb logger + result <- process logger opts.limit brigClient + Log.info logger $ "result" .= show result + where + initLogger = + Log.new + . Log.setLogLevel Log.Info + . Log.setOutput Log.StdOut + . Log.setFormat Nothing + . Log.setBufSize 0 + $ Log.defSettings + initCas settings l = + C.init + . C.setLogger (C.mkLogger l) + . C.setContacts settings.host [] + . C.setPortNumber (fromIntegral settings.port) + . C.setKeyspace settings.keyspace + . C.setProtocolVersion C.V4 + $ C.defSettings + desc = header "mls-users" <> progDesc "This program scans brig's users table and finds active users that don't support MLS" <> fullDesc + +-------------------------------------------------------------------------------- +-- queries + +lookupClientsLastActiveTimestamps :: ClientState -> UserId -> IO [Maybe UTCTime] +lookupClientsLastActiveTimestamps client u = do + runClient client $ runIdentity <$$> retry x1 (query selectClients (params One (Identity u))) + where + selectClients :: PrepQuery R (Identity UserId) (Identity (Maybe UTCTime)) + selectClients = "SELECT last_active from clients where user = ?" + +readUsers :: ClientState -> ConduitM () [UserRow] IO () +readUsers client = + transPipe (runClient client) (paginateC selectUsersAll (paramsP One () 1000) x5) + .| Conduit.map (fmap CQL.asRecord) + where + selectUsersAll :: C.PrepQuery C.R () (CQL.TupleType UserRow) + selectUsersAll = + "SELECT id, activated, status, supported_protocols FROM user" diff --git a/tools/db/mls-users/src/MlsUsers/Types.hs b/tools/db/mls-users/src/MlsUsers/Types.hs new file mode 100644 index 00000000000..49f219afd55 --- /dev/null +++ b/tools/db/mls-users/src/MlsUsers/Types.hs @@ -0,0 +1,123 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 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 MlsUsers.Types where + +import qualified Cassandra as C +import Control.Lens +import qualified Data.Aeson as A +import qualified Data.Aeson.Encode.Pretty as A +import qualified Data.ByteString.Lazy.Char8 as LC8 +import Data.Id +import Data.Text.Strict.Lens +import Database.CQL.Protocol (Record (..), TupleType, recordInstance) +import Imports +import Options.Applicative +import Wire.API.User + +data UserRow = UserRow + { userId :: UserId, + activated :: Bool, + status :: Maybe AccountStatus, + supportedProtocols :: Set BaseProtocolTag + } + deriving (Generic) + +instance A.ToJSON UserRow + +recordInstance ''UserRow + +instance Show UserRow where + show = LC8.unpack . A.encodePretty + +data Result = Result + { totalUsers :: Int, + activeNoMLS :: Int + } + deriving (Generic) + +instance A.ToJSON Result + +instance Show Result where + show = LC8.unpack . A.encodePretty + +instance Semigroup Result where + r1 <> r2 = + Result + { totalUsers = r1.totalUsers + r2.totalUsers, + activeNoMLS = r1.activeNoMLS + r2.activeNoMLS + } + +instance Monoid Result where + mempty = Result {totalUsers = 0, activeNoMLS = 0} + +data CassandraSettings = CassandraSettings + { host :: String, + port :: Int, + keyspace :: C.Keyspace + } + +data Opts = Opts + { brigDb :: CassandraSettings, + limit :: Maybe Int + } + +optsParser :: Parser Opts +optsParser = + Opts + <$> brigCassandraParser + <*> optional + ( option + auto + ( long "limit" + <> short 'l' + <> metavar "INT" + <> help "Limit the number of users to process" + ) + ) + +brigCassandraParser :: Parser CassandraSettings +brigCassandraParser = + CassandraSettings + <$> strOption + ( long "brig-cassandra-host" + <> metavar "HOST" + <> help "Cassandra Host for brig" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "brig-cassandra-port" + <> metavar "PORT" + <> help "Cassandra Port for brig" + <> value 9042 + <> showDefault + ) + <*> ( C.Keyspace + . view packed + <$> strOption + ( long "brig-cassandra-keyspace" + <> metavar "STRING" + <> help "Cassandra Keyspace for brig" + <> value "brig_test" + <> showDefault + ) + ) From 0bfd8ff890f7a8452008e507c1e462818cd87bd8 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 12 Dec 2025 16:15:59 +0100 Subject: [PATCH 26/60] WPB-22168 [fix-up] set the defaults of the cells feature correctly (#4907) --- changelog.d/2-features/WPB-22168 | 2 +- charts/galley/values.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog.d/2-features/WPB-22168 b/changelog.d/2-features/WPB-22168 index a1b41959ce2..0d2e1e27003 100644 --- a/changelog.d/2-features/WPB-22168 +++ b/changelog.d/2-features/WPB-22168 @@ -1 +1 @@ -New team feature config `cellsInternal` +New team feature config `cellsInternal` (#4889, #4907) diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 5c8aeee2728..03f5d5961cc 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -217,8 +217,8 @@ config: lockStatus: locked cells: defaults: - status: enabled - lockStatus: unlocked + status: disabled + lockStatus: locked cellsInternal: defaults: status: enabled From ec757750063179cb990a8df75820b16452552431 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 15 Dec 2025 16:52:17 +0100 Subject: [PATCH 27/60] Resolve race condition in integration test. (#4905) --- integration/test/Test/Spar/STM.hs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/integration/test/Test/Spar/STM.hs b/integration/test/Test/Spar/STM.hs index 0cbef532a2d..cbf8fb0c3a0 100644 --- a/integration/test/Test/Spar/STM.hs +++ b/integration/test/Test/Spar/STM.hs @@ -9,11 +9,12 @@ -- and thus get property-based integration tests! module Test.Spar.STM (testCreateIdpsAndScimsV7) where -import API.BrigInternal (getInvitationByEmail) +import API.BrigInternal import API.Common (defPassword) import API.GalleyInternal (setTeamFeatureStatus) import API.Nginz (login) import API.Spar +import Control.Retry import qualified Data.Map as Map import qualified SAML2.WebSSO as SAML import SetupHelpers @@ -235,7 +236,16 @@ validateStateLoginAllUsers owner tid state = do void $ loginWithSamlEmail True tid email idp bindResponse (deleteScimUser owner (unScimToken tok) uid) $ \resp -> do resp.status `shouldMatchInt` 204 + waitForUserGone uid void $ loginWithSamlEmail False tid email idp + where + waitForUserGone :: String -> App () + waitForUserGone uid = do + let pol = limitRetriesByCumulativeDelay 12_000_000 (fullJitterBackoff 5_000) + void $ recoverAll pol $ const do + getUsersId OwnDomain [uid] `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` ([] :: [()]) validateError :: Response -> Int -> String -> App () validateError resp errStatus errLabel = do From 36c31d4dfd36a34698f299256cb3b331e3520040 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 16 Dec 2025 14:55:23 +0100 Subject: [PATCH 28/60] Move code URI from ConversationCode to ConversationCodeInfo (#4911) This is more semantically correct and allows us to guarantee that there will always be a URI returned with the code. --- changelog.d/3-bug-fixes/conv-code-doc-fix | 2 + .../src/Wire/API/Conversation/Code.hs | 14 ++---- .../Golden/Generated/ConversationCode_user.hs | 48 ++----------------- .../Golden/Generated/Event_conversation.hs | 4 +- .../Wire/API/Golden/Generated/Event_user.hs | 24 ++++++++-- .../testObject_ConversationCode_user_1.json | 3 +- .../test/golden/testObject_Event_user_14.json | 3 +- services/galley/src/Galley/API/Query.hs | 2 +- services/galley/test/integration/API.hs | 17 +++---- 9 files changed, 42 insertions(+), 75 deletions(-) create mode 100644 changelog.d/3-bug-fixes/conv-code-doc-fix diff --git a/changelog.d/3-bug-fixes/conv-code-doc-fix b/changelog.d/3-bug-fixes/conv-code-doc-fix new file mode 100644 index 00000000000..8bafb87673f --- /dev/null +++ b/changelog.d/3-bug-fixes/conv-code-doc-fix @@ -0,0 +1,2 @@ +Fix swagger docs for `GET` and `POST` on `/conversations/{cnv}/code` to show +that the response will always include the `uri` field. \ No newline at end of file diff --git a/libs/wire-api/src/Wire/API/Conversation/Code.hs b/libs/wire-api/src/Wire/API/Conversation/Code.hs index 51a142ddd09..c80b588c535 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Code.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Code.hs @@ -83,8 +83,7 @@ instance ToSchema JoinConversationByCode where data ConversationCode = ConversationCode { conversationKey :: Code.Key, - conversationCode :: Code.Value, - conversationUri :: Maybe HttpsUrl + conversationCode :: Code.Value } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConversationCode) @@ -103,13 +102,6 @@ conversationCodeObjectSchema = "code" (description ?~ "Conversation code (random)") schema - <*> conversationUri - .= maybe_ - ( optFieldWithDocModifier - "uri" - (description ?~ "Full URI (containing key/code) to join a conversation") - schema - ) instance ToSchema ConversationCode where schema = @@ -120,6 +112,7 @@ instance ToSchema ConversationCode where data ConversationCodeInfo = ConversationCodeInfo { code :: ConversationCode, + uri :: HttpsUrl, hasPassword :: Bool } deriving stock (Eq, Show, Generic) @@ -133,11 +126,12 @@ instance ToSchema ConversationCodeInfo where (description ?~ "Contains conversation properties to update") $ ConversationCodeInfo <$> (.code) .= conversationCodeObjectSchema + <*> (.uri) .= (fieldWithDocModifier "uri" (description ?~ "Full URI (containing key/code) to join a conversation") schema) <*> (.hasPassword) .= fieldWithDocModifier "has_password" (description ?~ "Whether the conversation has a password") schema mkConversationCodeInfo :: Bool -> Code.Key -> Code.Value -> HttpsUrl -> ConversationCodeInfo mkConversationCodeInfo hasPw k v (HttpsUrl prefix) = - ConversationCodeInfo (ConversationCode k v (Just (HttpsUrl link))) hasPw + ConversationCodeInfo (ConversationCode k v) (HttpsUrl link) hasPw where q = [("key", toByteString' k), ("code", toByteString' v)] link = prefix & (URI.queryL . URI.queryPairsL) .~ q 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 27c4ed16765..787d45325d6 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 @@ -1,5 +1,3 @@ -{-# LANGUAGE OverloadedLists #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -20,61 +18,21 @@ module Test.Wire.API.Golden.Generated.ConversationCode_user where import Data.Code (Key (Key, asciiKey), Value (Value, asciiValue)) -import Data.Coerce (coerce) -import Data.Misc (HttpsUrl (HttpsUrl)) import Data.Range (unsafeRange) import Data.Text.Ascii (AsciiChars (validate)) -import Imports (Maybe (Just, Nothing), fromRight, undefined) -import URI.ByteString - ( Authority - ( Authority, - authorityHost, - authorityPort, - authorityUserInfo - ), - Host (Host, hostBS), - Query (Query, queryPairs), - Scheme (Scheme, schemeBS), - URIRef - ( URI, - uriAuthority, - uriFragment, - uriPath, - uriQuery, - uriScheme - ), - ) +import Imports (fromRight, undefined) import Wire.API.Conversation.Code (ConversationCode (..)) testObject_ConversationCode_user_1 :: ConversationCode testObject_ConversationCode_user_1 = ConversationCode { conversationKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "M0vnbETaqAgL8tv5Z1_x"))}, - conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "sEG3Y60tIsd9P3"))}, - conversationUri = - Just - ( coerce - URI - { uriScheme = Scheme {schemeBS = "https"}, - uriAuthority = - Just - ( Authority - { authorityUserInfo = Nothing, - authorityHost = Host {hostBS = "example.com"}, - authorityPort = Nothing - } - ), - uriPath = "", - uriQuery = Query {queryPairs = []}, - uriFragment = Nothing - } - ) + conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "sEG3Y60tIsd9P3"))} } testObject_ConversationCode_user_2 :: ConversationCode 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 + conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "lLz-9vR8ENum0kI-xWJs"))} } 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 39de347d68b..4b1476f5666 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 @@ -83,10 +83,10 @@ testObject_Event_conversation_3 = ( ConversationCodeInfo ( 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}) + conversationCode = Value {asciiValue = unsafeRange "7d6713"} } ) + (HttpsUrl (URI {uriScheme = Scheme {schemeBS = "https"}, uriAuthority = Just (Authority {authorityUserInfo = Nothing, authorityHost = Host {hostBS = "example.com"}, authorityPort = Nothing}), uriPath = "", uriQuery = Query {queryPairs = []}, uriFragment = Nothing})) False ), evtTeam = 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 36e84bf3489..e315b846ceb 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 @@ -21,13 +21,14 @@ module Test.Wire.API.Golden.Generated.Event_user where import Data.Domain import Data.Id -import Data.Misc (Milliseconds (Ms, ms)) +import Data.Misc (HttpsUrl (..), Milliseconds (Ms, ms)) import Data.Qualified import Data.Range (unsafeRange) import Data.Set qualified as Set import Data.Text.Ascii (validate) import Data.UUID qualified as UUID (fromString) import Imports +import URI.ByteString import Wire.API.Conversation import Wire.API.Conversation.CellsState import Wire.API.Conversation.Code @@ -308,14 +309,31 @@ testObject_Event_user_14 = Nothing (EdConvCodeUpdate cc) where + testURI :: HttpsUrl + testURI = + HttpsUrl + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "example.com"}, + authorityPort = Nothing + } + ), + uriPath = "", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } cc = ConversationCodeInfo ( ConversationCode { conversationKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "NEN=eLUWHXclTp=_2Nap"))}, - conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "lLz-9vR8ENum0kI-xWJs"))}, - conversationUri = Nothing + conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "lLz-9vR8ENum0kI-xWJs"))} } ) + testURI False testObject_Event_user_15 :: Event diff --git a/libs/wire-api/test/golden/testObject_ConversationCode_user_1.json b/libs/wire-api/test/golden/testObject_ConversationCode_user_1.json index 784e20ffcef..2fd637f6d0b 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCode_user_1.json +++ b/libs/wire-api/test/golden/testObject_ConversationCode_user_1.json @@ -1,5 +1,4 @@ { "code": "sEG3Y60tIsd9P3", - "key": "M0vnbETaqAgL8tv5Z1_x", - "uri": "https://example.com" + "key": "M0vnbETaqAgL8tv5Z1_x" } diff --git a/libs/wire-api/test/golden/testObject_Event_user_14.json b/libs/wire-api/test/golden/testObject_Event_user_14.json index 67fcad8fce1..624aef30e99 100644 --- a/libs/wire-api/test/golden/testObject_Event_user_14.json +++ b/libs/wire-api/test/golden/testObject_Event_user_14.json @@ -3,7 +3,8 @@ "data": { "code": "lLz-9vR8ENum0kI-xWJs", "has_password": false, - "key": "NEN=eLUWHXclTp=_2Nap" + "key": "NEN=eLUWHXclTp=_2Nap", + "uri": "https://example.com" }, "from": "0000114a-0000-7da8-0000-40cb00007fcf", "qualified_conversation": { diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 4dba67cd2c3..610c79be947 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -652,7 +652,7 @@ getConversationByReusableCode :: Value -> Sem r ConversationCoverView getConversationByReusableCode lusr key value = do - c <- verifyReusableCode (RateLimitUser (tUnqualified lusr)) False Nothing (ConversationCode key value Nothing) + c <- verifyReusableCode (RateLimitUser (tUnqualified lusr)) False Nothing (ConversationCode key value) conv <- E.getConversation (codeConversation c) >>= noteS @'ConvNotFound ensureConversationAccess (tUnqualified lusr) conv CodeAccess ensureGuestLinksEnabled (Data.convTeam conv) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 30a00c15d7a..e0f65f5d3af 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1186,24 +1186,19 @@ postJoinCodeConvOk = do info <- decodeConvCodeEvent <$> postConvCode alice conv let cCode = info.code liftIO $ info.hasPassword @?= 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 - -- TODO: Should there be two different types? - let payload = cCode {conversationUri = Nothing} -- unnecessary step, cCode can be posted as-is also. - incorrectCode = cCode {conversationCode = Code.Value (unsafeRange (Ascii.encodeBase64Url "incorrect-code"))} + let incorrectCode = cCode {conversationCode = Code.Value (unsafeRange (Ascii.encodeBase64Url "incorrect-code"))} -- with ActivatedAccess, bob can join, but not eve WS.bracketR2 c alice bob $ \(wsA, wsB) -> do -- incorrect code/key does not work postJoinCodeConv bob incorrectCode !!! const 404 === statusCode -- correct code works - postJoinCodeConv bob payload !!! const 200 === statusCode + postJoinCodeConv bob cCode !!! const 200 === statusCode -- non-admin cannot create invite link postConvCode bob conv !!! const 403 === statusCode -- test no-op - postJoinCodeConv bob payload !!! const 204 === statusCode + postJoinCodeConv bob cCode !!! const 204 === statusCode -- eve cannot join - postJoinCodeConv eve payload !!! const 403 === statusCode + postJoinCodeConv eve cCode !!! const 403 === statusCode void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB] $ wsAssertMemberJoinWithRole qconv qbob [qbob] roleNameWireMember @@ -1211,13 +1206,13 @@ postJoinCodeConvOk = do Right accessRolesWithGuests <- liftIO $ genAccessRolesV2 [TeamMemberAccessRole, NonTeamMemberAccessRole, GuestAccessRole] [] let nonActivatedAccess = ConversationAccessData (Set.singleton CodeAccess) accessRolesWithGuests putQualifiedAccessUpdate alice qconv nonActivatedAccess !!! const 200 === statusCode - postJoinCodeConv eve payload !!! const 200 === statusCode + postJoinCodeConv eve cCode !!! const 200 === statusCode -- guest cannot create invite link postConvCode eve conv !!! const 403 === statusCode -- after removing CodeAccess, no further people can join let noCodeAccess = ConversationAccessData (Set.singleton InviteAccess) accessRoles putQualifiedAccessUpdate alice qconv noCodeAccess !!! const 200 === statusCode - postJoinCodeConv dave payload !!! const 404 === statusCode + postJoinCodeConv dave cCode !!! const 404 === statusCode -- @END From ae0e5f6d29a7bea3dd583bda8688bbe162ee2013 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 18 Dec 2025 11:26:03 +0100 Subject: [PATCH 29/60] local-setup: Allow versioned calls to /register via nginz (#4914) --- services/nginz/integration-test/conf/nginz/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index efecea77fb2..4b9356f07c5 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -218,7 +218,7 @@ http { proxy_pass http://brig; } - location /register { + location ~* ^(/v[0-9]+)?/register { include common_response_no_zauth.conf; proxy_pass http://brig; } From 53174bc0ecdff7a517494e30f934060d0836565c Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 18 Dec 2025 14:45:36 +0100 Subject: [PATCH 30/60] WPB-22170 backend additional config values in cells feature flag (#4903) --- changelog.d/0-release-notes/WPB-22170 | 1 + changelog.d/1-api-changes/WPB-22170 | 1 + changelog.d/2-features/WPB-22170 | 1 + charts/galley/values.yaml | 32 ++ .../src/developer/reference/config-options.md | 42 +- integration/test/Test/Cells.hs | 27 +- integration/test/Test/FeatureFlags/Cells.hs | 90 +++- .../test/Test/FeatureFlags/CellsInternal.hs | 9 +- integration/test/Test/FeatureFlags/Util.hs | 65 ++- libs/schema-profunctor/src/Data/Schema.hs | 4 + libs/types-common/src/Data/Json/Util.hs | 53 +++ libs/types-common/test/Test/Properties.hs | 5 + .../Wire/API/Routes/Public/Galley/Feature.hs | 24 +- libs/wire-api/src/Wire/API/Team/Feature.hs | 442 +++++++++++++++--- .../Wire/UserSubsystem/InterpreterSpec.hs | 2 +- services/galley/galley.integration.yaml | 32 ++ .../galley/src/Galley/API/Public/Feature.hs | 4 +- tools/stern/src/Stern/API.hs | 2 +- tools/stern/src/Stern/API/Routes.hs | 2 +- tools/stern/test/integration/API.hs | 61 ++- 20 files changed, 788 insertions(+), 111 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-22170 create mode 100644 changelog.d/1-api-changes/WPB-22170 create mode 100644 changelog.d/2-features/WPB-22170 diff --git a/changelog.d/0-release-notes/WPB-22170 b/changelog.d/0-release-notes/WPB-22170 new file mode 100644 index 00000000000..d5b1b991432 --- /dev/null +++ b/changelog.d/0-release-notes/WPB-22170 @@ -0,0 +1 @@ +Operators: if you override `galley.settings.featureFlags.cells` in your Helm values, update your override to include the newly required cells config fields (channels/groups/one2one/users/collabora/publicLinks/storage/metadata); if you use the chart defaults, no action is needed. diff --git a/changelog.d/1-api-changes/WPB-22170 b/changelog.d/1-api-changes/WPB-22170 new file mode 100644 index 00000000000..fce4d607389 --- /dev/null +++ b/changelog.d/1-api-changes/WPB-22170 @@ -0,0 +1 @@ +The `PUT /teams/:tid/features/cells` endpoint has changed in API version V14 and requires additional config values. diff --git a/changelog.d/2-features/WPB-22170 b/changelog.d/2-features/WPB-22170 new file mode 100644 index 00000000000..617aff42949 --- /dev/null +++ b/changelog.d/2-features/WPB-22170 @@ -0,0 +1 @@ +The `cells` feature flag now contains a set of additional configuration values diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 03f5d5961cc..d0ed584bf67 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -219,6 +219,38 @@ config: defaults: status: disabled lockStatus: locked + config: + channels: + enabled: true + default: enabled + groups: + enabled: true + default: enabled + one2one: + enabled: true + default: enabled + users: + externals: true + guests: false + collabora: + enabled: false + publicLinks: + enableFiles: true + enableFolders: true + enforcePassword: false + enforceExpirationMax: 0 + enforceExpirationDefault: 0 + storage: + perFileQuotaBytes: "100000000" + recycle: + autoPurgeDays: 30 + disable: false + allowSkip: false + metadata: + namespaces: + usermetaTags: + defaultValues: [] + allowFreeValues: true cellsInternal: defaults: status: enabled diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index cb123a3b5a6..e6c126e997d 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -579,11 +579,43 @@ cells: defaults: status: disabled lockStatus: locked -``` - -### Cells Interna - -Cells configuration is intentionally split: `cells` is controlled by the team admin, while `cellsInternal` is set by the site operator/customer support via the internal API only. For `cellsInternal`, the `status` and `lockStatus` fields are *required* to be set to `enabled` and `unlocked` respectively, as enforced by validation logic. Failure to set these values will result in a configuration error. This block holds the backend URL, Collabora edition, and a storage quota. The quota must be provided as a positive decimal string that fits in `Int64` bytes. + config: + channels: + enabled: true + default: enabled + groups: + enabled: true + default: enabled + one2one: + enabled: true + default: enabled + users: + externals: true + guests: false + collabora: + enabled: false + publicLinks: + enableFiles: true + enableFolders: true + enforcePassword: false + enforceExpirationMax: 0 + enforceExpirationDefault: 0 + storage: + perFileQuotaBytes: "100000000" + recycle: + autoPurgeDays: 30 + disable: false + allowSkip: false + metadata: + namespaces: + usermetaTags: + defaultValues: [] + allowFreeValues: true +``` + +### Cells Internal + +Cells configuration is intentionally split: `cells` is controlled by the team admin, while `cellsInternal` is set by the site operator/customer support via the internal API only. For `cellsInternal`, the `status` and `lockStatus` fields are *required* to be set to `enabled` and `unlocked` respectively, as enforced by validation logic. Failure to set these values will result in a configuration error. This block holds the backend URL, Collabora edition, and a storage quota. The quota must be provided as a positive decimal string. ```yaml # galley.yaml diff --git a/integration/test/Test/Cells.hs b/integration/test/Test/Cells.hs index fad0d12c4b3..4efeacab5dc 100644 --- a/integration/test/Test/Cells.hs +++ b/integration/test/Test/Cells.hs @@ -42,7 +42,7 @@ testCellsEvent :: (HasCallStack) => App () testCellsEvent = do (alice, tid, [bob, chaz, dean, eve]) <- createTeam OwnDomain 5 conv <- postConversation alice defProteus {team = Just tid} >>= getJSON 201 - q <- watchCellsEvents (convEvents conv) + q <- watchCellsEventsForTeam tid (convEvents conv) bobId <- bob %. "qualified_id" chazId <- chaz %. "qualified_id" @@ -80,9 +80,8 @@ testCellsEvent = do testCellsCreationEvent :: (HasCallStack) => App () testCellsCreationEvent = do - -- start watcher before creating conversation - q0 <- watchCellsEvents def (alice, tid, _) <- createTeam OwnDomain 1 + q0 <- watchCellsEventsForTeam tid def conv <- postConversation alice defProteus {team = Just tid, cells = True} >>= getJSON 201 let q = q0 {filter = isNotifConv conv} :: QueueConsumer @@ -96,9 +95,8 @@ testCellsCreationEvent = do testCellsDeletionEvent :: (HasCallStack) => App () testCellsDeletionEvent = do - -- start watcher before creating conversation - q0 <- watchCellsEvents def (alice, tid, _) <- createTeam OwnDomain 1 + q0 <- watchCellsEventsForTeam tid def conv <- postConversation alice defProteus {team = Just tid, cells = True} >>= getJSON 201 void $ deleteTeamConversation tid conv alice >>= assertSuccess @@ -116,9 +114,8 @@ testCellsDeletionEvent = do testCellsCreationEventIsSentOnlyOnce :: (HasCallStack) => App () testCellsCreationEventIsSentOnlyOnce = do - -- start watcher before creating conversation - q0 <- watchCellsEvents def (alice, tid, members) <- createTeam OwnDomain 2 + q0 <- watchCellsEventsForTeam tid def conv <- postConversation alice defProteus {team = Just tid, cells = True, qualifiedUsers = members} >>= getJSON 201 let q = q0 {filter = isNotifConv conv} :: QueueConsumer @@ -141,16 +138,16 @@ testCellsFeatureCheck = do testCellsEventOnFeatureToggle :: (HasCallStack) => App () testCellsEventOnFeatureToggle = do - q0 <- watchCellsEvents def (_, tid, _) <- createTeam OwnDomain 1 + q <- watchCellsEventsForTeam tid def I.patchTeamFeatureConfig OwnDomain tid "cells" (object ["status" .= "disabled"]) >>= assertSuccess - getMessage q0 >>= \event -> do + getMessage q >>= \event -> do event %. "payload.0.type" `shouldMatch` "feature-config.update" event %. "payload.0.name" `shouldMatch` "cells" event %. "payload.0.team" `shouldMatch` (asString tid) event %. "payload.0.data.status" `shouldMatch` "disabled" I.patchTeamFeatureConfig OwnDomain tid "cells" (object ["status" .= "enabled"]) >>= assertSuccess - getMessage q0 >>= \event -> do + getMessage q >>= \event -> do event %. "payload.0.type" `shouldMatch` "feature-config.update" event %. "payload.0.name" `shouldMatch` "cells" event %. "payload.0.team" `shouldMatch` (asString tid) @@ -169,7 +166,7 @@ testCellsIgnoredEvents = do (alice, tid, _) <- createTeam OwnDomain 1 conv <- postConversation alice defProteus {team = Just tid} >>= getJSON 201 I.setCellsState alice conv "ready" >>= assertSuccess - q <- watchCellsEvents (convEvents conv) + q <- watchCellsEventsForTeam tid (convEvents conv) void $ updateMessageTimer alice conv 1000 >>= getBody 200 assertNoMessage q @@ -307,3 +304,11 @@ watchCellsEvents opts = do watcher <- ensureWatcher domain chan <- liftIO $ atomically $ dupTChan watcher.broadcast pure QueueConsumer {filter = opts.filter, chan} + +watchCellsEventsForTeam :: String -> WatchCellsEvents -> App QueueConsumer +watchCellsEventsForTeam tid opts = do + q <- watchCellsEvents opts + let isEventForTeam v = fieldEquals @Value v "payload.0.team" tid + -- the cells event queue is shared by tests + -- let's hope this filter reduces the risk of tests interfering with each other + pure $ q {filter = isEventForTeam} diff --git a/integration/test/Test/FeatureFlags/Cells.hs b/integration/test/Test/FeatureFlags/Cells.hs index 98fab115b76..0e622da0001 100644 --- a/integration/test/Test/FeatureFlags/Cells.hs +++ b/integration/test/Test/FeatureFlags/Cells.hs @@ -17,16 +17,98 @@ module Test.FeatureFlags.Cells where +import API.Galley (setTeamFeatureConfigVersioned) +import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude testCells :: (HasCallStack) => APIAccess -> App () testCells access = mkFeatureTests "cells" - & addUpdate enabled - & addUpdate disabled - & addInvalidUpdate (object []) + & addUpdate (validConfig True) + & addUpdate (validConfig False) + & addInvalidUpdate invalidConfig & runFeatureTests OwnDomain access testPatchCells :: (HasCallStack) => App () -testPatchCells = checkPatch OwnDomain "cells" enabled +testPatchCells = checkPatch OwnDomain "cells" (validConfig True) + +validConfig :: Bool -> Value +validConfig b = + object + [ "status" .= if b then "enabled" else "disabled", + "config" + .= object + [ "channels" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "groups" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "one2one" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "users" + .= object + [ "externals" .= True, + "guests" .= False + ], + "collabora" + .= object + ["enabled" .= False], + "publicLinks" + .= object + [ "enableFiles" .= True, + "enableFolders" .= True, + "enforcePassword" .= False, + "enforceExpirationMax" .= (0 :: Int), + "enforceExpirationDefault" .= (0 :: Int) + ], + "storage" + .= object + [ "perFileQuotaBytes" .= "100000000", + "recycle" + .= object + [ "autoPurgeDays" .= (30 :: Int), + "disable" .= False, + "allowSkip" .= False + ] + ], + "metadata" + .= object + [ "namespaces" + .= object + [ "usermetaTags" + .= object + [ "defaultValues" .= ([] :: [String]), + "allowFreeValues" .= True + ] + ] + ] + ] + ] + +invalidConfig :: Value +invalidConfig = + object + [ "status" .= "enabled", + "config" .= object ["foox" .= "bar"] + ] + +testCellsV13 :: (HasCallStack) => App () +testCellsV13 = do + (alice, tid, _) <- createTeam OwnDomain 1 + setTeamFeatureConfigVersioned + (ExplicitVersion 13) + alice + tid + "cells" + enabled + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 diff --git a/integration/test/Test/FeatureFlags/CellsInternal.hs b/integration/test/Test/FeatureFlags/CellsInternal.hs index 550d079d140..fa7cef27f05 100644 --- a/integration/test/Test/FeatureFlags/CellsInternal.hs +++ b/integration/test/Test/FeatureFlags/CellsInternal.hs @@ -19,19 +19,14 @@ module Test.FeatureFlags.CellsInternal where import qualified API.GalleyInternal as Internal import SetupHelpers -import Test.Cells (QueueConsumer (..), getMessage, watchCellsEvents) +import Test.Cells (getMessage, watchCellsEventsForTeam) import Test.FeatureFlags.Util import Testlib.Prelude testCellsInternalEvent :: (HasCallStack) => App () testCellsInternalEvent = do (alice, tid, _) <- createTeam OwnDomain 0 - q <- do - q <- watchCellsEvents def - let isEventForTeam v = fieldEquals @Value v "payload.0.team" tid - -- the cells event queue is shared by tests - -- let's hope this filter reduces the risk of tests interfering with each other - pure $ q {filter = isEventForTeam} + q <- watchCellsEventsForTeam tid def let quota = "234723984" update = mkFt "enabled" "unlocked" defConf {quota} setFeature InternalAPI alice tid "cellsInternal" update >>= assertSuccess diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 7ab2eebc654..3c7f617d8ca 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -69,6 +69,63 @@ defEnabledObj conf = "config" .= conf ] +defCellsConfig :: Value +defCellsConfig = + object + [ "channels" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "groups" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "one2one" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "users" + .= object + [ "externals" .= True, + "guests" .= False + ], + "collabora" + .= object + ["enabled" .= False], + "publicLinks" + .= object + [ "enableFiles" .= True, + "enableFolders" .= True, + "enforcePassword" .= False, + "enforceExpirationMax" .= (0 :: Int), + "enforceExpirationDefault" .= (0 :: Int) + ], + "storage" + .= object + [ "perFileQuotaBytes" .= "100000000", + "recycle" + .= object + [ "autoPurgeDays" .= (30 :: Int), + "disable" .= False, + "allowSkip" .= False + ] + ], + "metadata" + .= object + [ "namespaces" + .= object + [ "usermetaTags" + .= object + [ "defaultValues" .= ([] :: [String]), + "allowFreeValues" .= True + ] + ] + ] + ] + defAllFeatures :: Value defAllFeatures = object @@ -149,7 +206,13 @@ defAllFeatures = "allowed_to_open_channels" .= "team-members" ] ], - "cells" .= enabled, + "cells" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "enabled", + "ttl" .= "unlimited", + "config" .= defCellsConfig + ], "assetAuditLog" .= disabledLocked, "allowedGlobalOperations" .= object diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index b02c3375f49..3cb88657d5b 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -30,6 +30,7 @@ module Data.Schema ObjectSchema, ObjectSchemaP, ToSchema (..), + ToObjectSchema (..), Schema (..), mkSchema, schemaDoc, @@ -852,6 +853,9 @@ instance HasOpt NamedSwaggerDoc where class ToSchema a where schema :: ValueSchema NamedSwaggerDoc a +class ToObjectSchema a where + objectSchema :: ObjectSchema SwaggerDoc a + -- Newtype wrappers for deriving via newtype Schema a = Schema {getSchema :: a} diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index cb387058b74..96a1f9ae6b7 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -46,6 +46,9 @@ module Data.Json.Util fromBase64TextLenient, fromBase64Text, toBase64Text, + + -- * Other + BigIntString (..), ) where @@ -65,9 +68,11 @@ import Data.ByteString.UTF8 qualified as UTF8 import Data.Fixed import Data.OpenApi qualified as S import Data.Schema +import Data.Text qualified as T import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.Text.Encoding.Error qualified as Text +import Data.Text.Read qualified as TR import Data.Time.Clock import Data.Time.Format (formatTime, parseTimeM) import Data.Time.Lens qualified as TL @@ -90,6 +95,54 @@ infixr 5 # (#) = append {-# INLINE (#) #-} +----------------------------------------------------------------------------- +-- BigIntString + +-- | A wrapper type for arbitrary-precision /signed/ integer values that must +-- be serialized and deserialized as decimal strings in JSON and OpenAPI +-- schemas. +-- +-- This type is intended for situations where numeric values may grow beyond +-- the safe integer range of JavaScript (2^53-1), and therefore cannot be +-- represented as JSON numbers without losing precision. Instead, values are +-- encoded as textual decimal representations, ensuring: +-- +-- * Arbitrary size support – backed by 'Integer', so values never overflow. +-- * Lossless JSON round-trips – encoded as JSON strings rather than numbers. +-- * Type-safe usage in APIs – OpenAPI schema ('ToSchema') reflects a +-- string-based representation with integer parsing rules. +-- +-- The textual form must be a (possibly negative) decimal integer without +-- fractional parts, for example: +-- +-- * @"0"@ +-- * @"42"@ +-- * @"-9000"@ +-- +-- This type is generic and not bound to any specific unit. It can represent +-- large counts of seconds, bytes, IDs, or any other quantity that must +-- remain precise end-to-end across systems with differing numeric +-- capabilities. +newtype BigIntString = BigIntString {unBigIntString :: Integer} + deriving (Show, Eq, Ord, Generic, Arbitrary) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema BigIntString + +instance ToSchema BigIntString where + schema = toText .= (BigIntString <$> bigNatStringSchema) + where + toText :: BigIntString -> Text + toText = T.pack . show . unBigIntString + + bigNatStringSchema :: ValueSchemaP NamedSwaggerDoc Text Integer + bigNatStringSchema = schema `withParser` p + where + p :: Text -> A.Parser Integer + p txt = do + (n, rest) <- either fail pure (TR.signed TR.decimal txt :: Either String (Integer, Text)) + unless (T.null rest) $ + fail "value must be an integer string without decimals" + pure n + ----------------------------------------------------------------------------- -- UTCTimeMillis diff --git a/libs/types-common/test/Test/Properties.hs b/libs/types-common/test/Test/Properties.hs index e8ce0320de7..1de6d1ac065 100644 --- a/libs/types-common/test/Test/Properties.hs +++ b/libs/types-common/test/Test/Properties.hs @@ -35,6 +35,7 @@ import Data.ByteString.Lazy as L import Data.Domain (Domain) import Data.Handle (Handle) import Data.Id +import Data.Json.Util import Data.Json.Util qualified as Util import Data.Nonce (Nonce) import Data.ProtocolBuffers.Internal @@ -217,6 +218,10 @@ tests = [ testProperty "decode . encode = id" $ \(x :: Nonce) -> bsRoundtrip x, jsonRoundtrip @Nonce + ], + testGroup + "BigIntString" + [ jsonRoundtrip @BigIntString ] ] diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index d08b09ce336..d0c02b34126 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -29,6 +29,7 @@ import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.Team.Feature import Wire.API.Team.SearchVisibility (TeamSearchVisibilityView) @@ -66,7 +67,9 @@ type FeatureAPI = :<|> AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs :<|> FeatureAPIGet DomainRegistrationConfig :<|> FeatureAPIGetPut ChannelsConfig - :<|> FeatureAPIGetPut CellsConfig + :<|> FeatureAPIGet CellsConfig + :<|> Until 'V14 ::> VersionedFeatureAPIPut "put-CellsConfig@v13" V13 CellsConfig + :<|> From 'V14 ::> FeatureAPIPut CellsConfig :<|> FeatureAPIGet AllowedGlobalOperationsConfig :<|> FeatureAPIGet AssetAuditLogConfig :<|> FeatureAPIGet ConsumableNotificationsConfig @@ -76,6 +79,25 @@ type FeatureAPI = :<|> FeatureAPIGet StealthUsersConfig :<|> FeatureAPIGet CellsInternalConfig +type VersionedFeatureAPIPut named reqBodyVersion cfg = + Named + named + ( Description (FeatureAPIDesc cfg) + :> ZUser + :> Summary (AppendSymbol "Put config for " (FeatureSymbol cfg)) + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamNotFound + :> CanThrow TeamFeatureError + :> CanThrowMany (FeatureErrors cfg) + :> "teams" + :> Capture "tid" TeamId + :> "features" + :> FeatureSymbol cfg + :> VersionedReqBody reqBodyVersion '[Servant.JSON] (Feature cfg) + :> Put '[Servant.JSON] (LockableFeature cfg) + ) + type DeprecationNotice1 = "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" type DeprecationNotice2 = "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 470a382258a..0e323507d49 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -85,7 +85,18 @@ module Wire.API.Team.Feature EnforceFileDownloadLocationConfig, LimitedEventFanoutConfig (..), DomainRegistrationConfig (..), - CellsConfig (..), + CellsConfig, + CellsConfigB (..), + CellsProperty (..), + CellsPropertyStatus (..), + CellsUsers (..), + CellsCollaboraStatus (..), + CellsPublicLinks (..), + CellsConfigStorage (..), + CellsRecycle (..), + CellsMetadata (..), + CellsNamespaces (..), + CellsUserMetaTags (..), CellsInternalConfig, CellsInternalConfigB (..), CellsCollabora (..), @@ -146,7 +157,6 @@ import Data.Text.Encoding qualified as T import Data.Text.Encoding.Error import Data.Text.Lazy qualified as TL import Data.Text.Lazy.Encoding qualified as TL -import Data.Text.Read qualified as TR import Data.Time import Deriving.Aeson import GHC.TypeLits @@ -161,6 +171,7 @@ import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.Routes.Named hiding (unnamed) import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.Team.Feature.Profunctor import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -217,6 +228,10 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- 'docs/src/understand/team-feature-settings.md') class ( Default cfg, + -- \| Should be "pure MyFeatureConfig" if the feature doesn't have config, + -- which results in a trivial empty schema and the "config" field being + -- omitted/ignored in the JSON encoder / parser. + ToObjectSchema cfg, ToSchema cfg, Default (LockableFeature cfg), KnownSymbol (FeatureSymbol cfg), @@ -228,12 +243,6 @@ class type FeatureSymbol cfg :: Symbol featureSingleton :: FeatureSingleton cfg - objectSchema :: - -- | Should be "pure MyFeatureConfig" if the feature doesn't have config, - -- which results in a trivial empty schema and the "config" field being - -- omitted/ignored in the JSON encoder / parser. - ObjectSchema SwaggerDoc cfg - data FeatureSingleton cfg where FeatureSingletonGuestLinksConfig :: FeatureSingleton GuestLinksConfig FeatureSingletonLegalholdConfig :: FeatureSingleton LegalholdConfig @@ -444,7 +453,7 @@ forgetLock ws = Feature ws.status ws.config withLockStatus :: LockStatus -> Feature a -> LockableFeature a withLockStatus ls (Feature s c) = LockableFeature s ls c -instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (Feature cfg) where +instance (ToSchema cfg, ToObjectSchema cfg) => ToSchema (Feature cfg) where schema = object name $ Feature @@ -458,6 +467,12 @@ instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (Feature cfg) where inner = schema @cfg name = fromMaybe "" (getName (schemaDoc inner)) <> ".Feature" +instance + (ToObjectSchema (Versioned v cfg), ToSchema (Versioned v cfg)) => + ToSchema (Versioned v (Feature cfg)) + where + schema = Versioned . fmap unVersioned <$> (fmap Versioned . unVersioned) .= schema @(Feature (Versioned v cfg)) + ---------------------------------------------------------------------- -- FeatureTTL @@ -627,12 +642,13 @@ instance ToSchema GuestLinksConfig where instance Default (LockableFeature GuestLinksConfig) where def = defUnlockedFeature +instance ToObjectSchema GuestLinksConfig where + objectSchema = pure GuestLinksConfig + instance IsFeatureConfig GuestLinksConfig where type FeatureSymbol GuestLinksConfig = "conversationGuestLinks" featureSingleton = FeatureSingletonGuestLinksConfig - objectSchema = pure GuestLinksConfig - -------------------------------------------------------------------------------- -- Legalhold feature @@ -645,10 +661,12 @@ data LegalholdConfig = LegalholdConfig instance Default (LockableFeature LegalholdConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema LegalholdConfig where + objectSchema = pure LegalholdConfig + instance IsFeatureConfig LegalholdConfig where type FeatureSymbol LegalholdConfig = "legalhold" featureSingleton = FeatureSingletonLegalholdConfig - objectSchema = pure LegalholdConfig instance ToSchema LegalholdConfig where schema = object "LegalholdConfig" objectSchema @@ -666,10 +684,12 @@ data SSOConfig = SSOConfig instance Default (LockableFeature SSOConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema SSOConfig where + objectSchema = pure SSOConfig + instance IsFeatureConfig SSOConfig where type FeatureSymbol SSOConfig = "sso" featureSingleton = FeatureSingletonSSOConfig - objectSchema = pure SSOConfig instance ToSchema SSOConfig where schema = object "SSOConfig" objectSchema @@ -688,10 +708,12 @@ data SearchVisibilityAvailableConfig = SearchVisibilityAvailableConfig instance Default (LockableFeature SearchVisibilityAvailableConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema SearchVisibilityAvailableConfig where + objectSchema = pure SearchVisibilityAvailableConfig + instance IsFeatureConfig SearchVisibilityAvailableConfig where type FeatureSymbol SearchVisibilityAvailableConfig = "searchVisibility" featureSingleton = FeatureSingletonSearchVisibilityAvailableConfig - objectSchema = pure SearchVisibilityAvailableConfig instance ToSchema SearchVisibilityAvailableConfig where schema = object "SearchVisibilityAvailableConfig" objectSchema @@ -714,10 +736,12 @@ instance ToSchema ValidateSAMLEmailsConfig where instance Default (LockableFeature ValidateSAMLEmailsConfig) where def = defUnlockedFeature +instance ToObjectSchema ValidateSAMLEmailsConfig where + objectSchema = pure ValidateSAMLEmailsConfig + instance IsFeatureConfig ValidateSAMLEmailsConfig where type FeatureSymbol ValidateSAMLEmailsConfig = "validateSAMLemails" featureSingleton = FeatureSingletonValidateSAMLEmailsConfig - objectSchema = pure ValidateSAMLEmailsConfig type instance DeprecatedFeatureName V2 ValidateSAMLEmailsConfig = "validate-saml-emails" @@ -734,10 +758,12 @@ data DigitalSignaturesConfig = DigitalSignaturesConfig instance Default (LockableFeature DigitalSignaturesConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema DigitalSignaturesConfig where + objectSchema = pure DigitalSignaturesConfig + instance IsFeatureConfig DigitalSignaturesConfig where type FeatureSymbol DigitalSignaturesConfig = "digitalSignatures" featureSingleton = FeatureSingletonDigitalSignaturesConfig - objectSchema = pure DigitalSignaturesConfig type instance DeprecatedFeatureName V2 DigitalSignaturesConfig = "digital-signatures" @@ -804,10 +830,12 @@ instance Default ConferenceCallingConfig where instance Default (LockableFeature ConferenceCallingConfig) where def = defLockedFeature {status = FeatureStatusEnabled} +instance ToObjectSchema ConferenceCallingConfig where + objectSchema = fromMaybe def <$> optField "config" schema + instance IsFeatureConfig ConferenceCallingConfig where type FeatureSymbol ConferenceCallingConfig = "conferenceCalling" featureSingleton = FeatureSingletonConferenceCallingConfig - objectSchema = fromMaybe def <$> optField "config" schema instance (OptWithDefault f) => ToSchema (ConferenceCallingConfigB Covered f) where schema = @@ -832,10 +860,12 @@ instance ToSchema SndFactorPasswordChallengeConfig where instance Default (LockableFeature SndFactorPasswordChallengeConfig) where def = defLockedFeature +instance ToObjectSchema SndFactorPasswordChallengeConfig where + objectSchema = pure SndFactorPasswordChallengeConfig + instance IsFeatureConfig SndFactorPasswordChallengeConfig where type FeatureSymbol SndFactorPasswordChallengeConfig = "sndFactorPasswordChallenge" featureSingleton = FeatureSingletonSndFactorPasswordChallengeConfig - objectSchema = pure SndFactorPasswordChallengeConfig -------------------------------------------------------------------------------- -- SearchVisibilityInbound feature @@ -850,10 +880,12 @@ data SearchVisibilityInboundConfig = SearchVisibilityInboundConfig instance Default (LockableFeature SearchVisibilityInboundConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema SearchVisibilityInboundConfig where + objectSchema = pure SearchVisibilityInboundConfig + instance IsFeatureConfig SearchVisibilityInboundConfig where type FeatureSymbol SearchVisibilityInboundConfig = "searchVisibilityInbound" featureSingleton = FeatureSingletonSearchVisibilityInboundConfig - objectSchema = pure SearchVisibilityInboundConfig instance ToSchema SearchVisibilityInboundConfig where schema = object "SearchVisibilityInboundConfig" objectSchema @@ -889,11 +921,12 @@ instance ToSchema ClassifiedDomainsConfig where instance Default (LockableFeature ClassifiedDomainsConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema ClassifiedDomainsConfig where + objectSchema = field "config" schema + instance IsFeatureConfig ClassifiedDomainsConfig where type FeatureSymbol ClassifiedDomainsConfig = "classifiedDomains" - featureSingleton = FeatureSingletonClassifiedDomainsConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- AppLock feature @@ -937,11 +970,12 @@ instance (FieldF f) => ToSchema (AppLockConfigB Covered f) where instance Default (LockableFeature AppLockConfig) where def = defUnlockedFeature +instance ToObjectSchema AppLockConfig where + objectSchema = field "config" schema + instance IsFeatureConfig AppLockConfig where type FeatureSymbol AppLockConfig = "appLock" - featureSingleton = FeatureSingletonAppLockConfig - objectSchema = field "config" schema newtype EnforceAppLock = EnforceAppLock Bool deriving stock (Eq, Show, Ord, Generic) @@ -966,10 +1000,12 @@ instance Default FileSharingConfig where instance Default (LockableFeature FileSharingConfig) where def = defUnlockedFeature +instance ToObjectSchema FileSharingConfig where + objectSchema = pure FileSharingConfig + instance IsFeatureConfig FileSharingConfig where type FeatureSymbol FileSharingConfig = "fileSharing" featureSingleton = FeatureSingletonFileSharingConfig - objectSchema = pure FileSharingConfig instance ToSchema FileSharingConfig where schema = object "FileSharingConfig" objectSchema @@ -1014,10 +1050,12 @@ instance (FieldF f) => ToSchema (SelfDeletingMessagesConfigB Covered f) where instance Default (LockableFeature SelfDeletingMessagesConfig) where def = defUnlockedFeature +instance ToObjectSchema SelfDeletingMessagesConfig where + objectSchema = field "config" schema + instance IsFeatureConfig SelfDeletingMessagesConfig where type FeatureSymbol SelfDeletingMessagesConfig = "selfDeletingMessages" featureSingleton = FeatureSingletonSelfDeletingMessagesConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- MLSConfig @@ -1083,10 +1121,12 @@ instance (FieldF f) => ToSchema (MLSConfigB Covered f) where instance Default (LockableFeature MLSConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema MLSConfig where + objectSchema = field "config" schema + instance IsFeatureConfig MLSConfig where type FeatureSymbol MLSConfig = "mls" featureSingleton = FeatureSingletonMLSConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- ChannelsConfig @@ -1144,10 +1184,12 @@ instance (FieldF f) => ToSchema (ChannelsConfigB Covered f) where instance Default (LockableFeature ChannelsConfig) where def = defLockedFeature +instance ToObjectSchema ChannelsConfig where + objectSchema = field "config" schema + instance IsFeatureConfig ChannelsConfig where type FeatureSymbol ChannelsConfig = "channels" featureSingleton = FeatureSingletonChannelsConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- ExposeInvitationURLsToTeamAdminConfig @@ -1161,10 +1203,12 @@ data ExposeInvitationURLsToTeamAdminConfig = ExposeInvitationURLsToTeamAdminConf instance Default (LockableFeature ExposeInvitationURLsToTeamAdminConfig) where def = defLockedFeature +instance ToObjectSchema ExposeInvitationURLsToTeamAdminConfig where + objectSchema = pure ExposeInvitationURLsToTeamAdminConfig + instance IsFeatureConfig ExposeInvitationURLsToTeamAdminConfig where type FeatureSymbol ExposeInvitationURLsToTeamAdminConfig = "exposeInvitationURLsToTeamAdmin" featureSingleton = FeatureSingletonExposeInvitationURLsToTeamAdminConfig - objectSchema = pure ExposeInvitationURLsToTeamAdminConfig instance ToSchema ExposeInvitationURLsToTeamAdminConfig where schema = object "ExposeInvitationURLsToTeamAdminConfig" objectSchema @@ -1183,10 +1227,12 @@ data OutlookCalIntegrationConfig = OutlookCalIntegrationConfig instance Default (LockableFeature OutlookCalIntegrationConfig) where def = defLockedFeature +instance ToObjectSchema OutlookCalIntegrationConfig where + objectSchema = pure OutlookCalIntegrationConfig + instance IsFeatureConfig OutlookCalIntegrationConfig where type FeatureSymbol OutlookCalIntegrationConfig = "outlookCalIntegration" featureSingleton = FeatureSingletonOutlookCalIntegrationConfig - objectSchema = pure OutlookCalIntegrationConfig instance ToSchema OutlookCalIntegrationConfig where schema = object "OutlookCalIntegrationConfig" objectSchema @@ -1283,10 +1329,12 @@ instance (FieldF f) => ToSchema (MlsE2EIdConfigB Covered f) where instance Default (LockableFeature MlsE2EIdConfig) where def = defLockedFeature +instance ToObjectSchema MlsE2EIdConfig where + objectSchema = field "config" schema + instance IsFeatureConfig MlsE2EIdConfig where type FeatureSymbol MlsE2EIdConfig = "mlsE2EId" featureSingleton = FeatureSingletonMlsE2EIdConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- MlsMigration @@ -1338,10 +1386,12 @@ instance (NestedMaybe f) => ToSchema (MlsMigrationConfigB Covered f) where instance Default (LockableFeature MlsMigrationConfig) where def = defLockedFeature +instance ToObjectSchema MlsMigrationConfig where + objectSchema = field "config" schema + instance IsFeatureConfig MlsMigrationConfig where type FeatureSymbol MlsMigrationConfig = "mlsMigration" featureSingleton = FeatureSingletonMlsMigrationConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- EnforceFileDownloadLocationConfig @@ -1388,10 +1438,12 @@ instance (NestedMaybe f) => ToSchema (EnforceFileDownloadLocationConfigB Covered instance Default (LockableFeature EnforceFileDownloadLocationConfig) where def = defLockedFeature +instance ToObjectSchema EnforceFileDownloadLocationConfig where + objectSchema = field "config" schema + instance IsFeatureConfig EnforceFileDownloadLocationConfig where type FeatureSymbol EnforceFileDownloadLocationConfig = "enforceFileDownloadLocation" featureSingleton = FeatureSingletonEnforceFileDownloadLocationConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- Guarding the fanout of events when a team member is deleted. @@ -1410,10 +1462,12 @@ data LimitedEventFanoutConfig = LimitedEventFanoutConfig instance Default (LockableFeature LimitedEventFanoutConfig) where def = defUnlockedFeature +instance ToObjectSchema LimitedEventFanoutConfig where + objectSchema = pure LimitedEventFanoutConfig + instance IsFeatureConfig LimitedEventFanoutConfig where type FeatureSymbol LimitedEventFanoutConfig = "limitedEventFanout" featureSingleton = FeatureSingletonLimitedEventFanoutConfig - objectSchema = pure LimitedEventFanoutConfig instance ToSchema LimitedEventFanoutConfig where schema = object "LimitedEventFanoutConfig" objectSchema @@ -1434,30 +1488,260 @@ instance ToSchema DomainRegistrationConfig where instance Default (LockableFeature DomainRegistrationConfig) where def = defLockedFeature +instance ToObjectSchema DomainRegistrationConfig where + objectSchema = pure DomainRegistrationConfig + instance IsFeatureConfig DomainRegistrationConfig where type FeatureSymbol DomainRegistrationConfig = "domainRegistration" featureSingleton = FeatureSingletonDomainRegistrationConfig - objectSchema = pure DomainRegistrationConfig -------------------------------------------------------------------------------- -- Cells feature -data CellsConfig = CellsConfig - deriving (Eq, Show, Generic, GSOP.Generic) - deriving (Arbitrary) via (GenericUniform CellsConfig) - deriving (RenderableSymbol) via (RenderableTypeName CellsConfig) - deriving (Default, ParseDbFeature) via (TrivialFeature CellsConfig) +data CellsPropertyStatus = Enabled | Disabled | Enforced + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsPropertyStatus + deriving (Arbitrary) via (GenericUniform CellsPropertyStatus) -instance ToSchema CellsConfig where - schema = object "CellsConfig" objectSchema +instance ToSchema CellsPropertyStatus where + schema = + enum @Text "CellsPropertyStatus" $ + mconcat + [ element "enabled" Enabled, + element "disabled" Disabled, + element "enforced" Enforced + ] + +data CellsProperty = CellsProperty + { enabled :: Bool, + default_ :: CellsPropertyStatus + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsProperty + deriving (Arbitrary) via (GenericUniform CellsProperty) + +instance ToSchema CellsProperty where + schema = + object "CellsProperty" $ + CellsProperty + <$> (.enabled) .= field "enabled" schema + <*> (.default_) .= field "default" schema + +data CellsUsers = CellsUsers + { externals :: Bool, + guests :: Bool + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsUsers + deriving (Arbitrary) via (GenericUniform CellsUsers) + +instance ToSchema CellsUsers where + schema = + object "CellsUsers" $ + CellsUsers + <$> (.externals) .= field "externals" schema + <*> (.guests) .= field "guests" schema + +newtype CellsCollaboraStatus = CellsCollaboraStatus {enabled :: Bool} + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsCollaboraStatus + deriving (Arbitrary) via (GenericUniform CellsCollaboraStatus) + +instance ToSchema CellsCollaboraStatus where + schema = + object "CellsCollaboraStatus" $ + CellsCollaboraStatus + <$> (.enabled) .= field "enabled" schema + +data CellsPublicLinks = CellsPublicLinks + { enableFiles :: Bool, + enableFolders :: Bool, + enforcePassword :: Bool, + enforceExpirationMax :: Int64, + enforceExpirationDefault :: Int64 + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsPublicLinks + deriving (Arbitrary) via (GenericUniform CellsPublicLinks) + +instance ToSchema CellsPublicLinks where + schema = + object "CellsPublicLinks" $ + CellsPublicLinks + <$> enableFiles .= field "enableFiles" schema + <*> enableFolders .= field "enableFolders" schema + <*> enforcePassword .= field "enforcePassword" schema + <*> enforceExpirationMax .= field "enforceExpirationMax" schema + <*> enforceExpirationDefault .= field "enforceExpirationDefault" schema + +data CellsRecycle = CellsRecycle + { autoPurgeDays :: Int, + disable :: Bool, + allowSkip :: Bool + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsRecycle + deriving (Arbitrary) via (GenericUniform CellsRecycle) + +instance ToSchema CellsRecycle where + schema = + object "CellsRecycle" $ + CellsRecycle + <$> autoPurgeDays .= field "autoPurgeDays" schema + <*> disable .= field "disable" schema + <*> allowSkip .= field "allowSkip" schema + +data CellsConfigStorage = CellsConfigStorage + { perFileQuotaBytes :: NumBytes, + recycle :: CellsRecycle + } + deriving (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsConfigStorage + deriving (Arbitrary) via (GenericUniform CellsConfigStorage) + +instance ToSchema CellsConfigStorage where + schema = + object "CellsConfigStorage" $ + CellsConfigStorage + <$> perFileQuotaBytes .= field "perFileQuotaBytes" schema + <*> recycle .= field "recycle" schema + +data CellsUserMetaTags = CellsUserMetaTags + { defaultValues :: [Text], + allowFreeValues :: Bool + } + deriving (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsUserMetaTags + deriving (Arbitrary) via (GenericUniform CellsUserMetaTags) + +instance ToSchema CellsUserMetaTags where + schema = + object "CellsUserMetaTags" $ + CellsUserMetaTags + <$> defaultValues .= field "defaultValues" (array schema) + <*> allowFreeValues .= field "allowFreeValues" schema + +newtype CellsNamespaces = CellsNamespaces {usermetaTags :: CellsUserMetaTags} + deriving (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsNamespaces + deriving (Arbitrary) via (GenericUniform CellsNamespaces) + +instance ToSchema CellsNamespaces where + schema = + object "CellsNamespaces" $ + CellsNamespaces + <$> usermetaTags .= field "usermetaTags" schema + +newtype CellsMetadata = CellsMetadata {namespaces :: CellsNamespaces} + deriving (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsMetadata + deriving (Arbitrary) via (GenericUniform CellsMetadata) + +instance ToSchema CellsMetadata where + schema = + object "CellsMetadata" $ + CellsMetadata + <$> namespaces .= field "namespaces" schema + +data CellsConfigB t f = CellsConfig + { channels :: Wear t f CellsProperty, + groups :: Wear t f CellsProperty, + one2one :: Wear t f CellsProperty, + users :: Wear t f CellsUsers, + collabora :: Wear t f CellsCollaboraStatus, + publicLinks :: Wear t f CellsPublicLinks, + storage :: Wear t f CellsConfigStorage, + metadata :: Wear t f CellsMetadata + } + deriving (Generic, BareB) + +deriving instance FunctorB (CellsConfigB Covered) + +deriving instance ApplicativeB (CellsConfigB Covered) + +deriving instance TraversableB (CellsConfigB Covered) + +type CellsConfig = CellsConfigB Bare Identity + +deriving instance Eq CellsConfig + +deriving instance Show CellsConfig + +deriving via (RenderableTypeName CellsConfig) instance (RenderableSymbol CellsConfig) + +deriving via (GenericUniform CellsConfig) instance (Arbitrary CellsConfig) + +deriving via (BarbieFeature CellsConfigB) instance (ParseDbFeature CellsConfig) + +deriving via (BarbieFeature CellsConfigB) instance (ToSchema CellsConfig) + +instance Default CellsConfig where + def = + CellsConfig + { channels = CellsProperty {enabled = True, default_ = Enabled}, + groups = CellsProperty {enabled = True, default_ = Enabled}, + one2one = CellsProperty {enabled = True, default_ = Enabled}, + users = CellsUsers {externals = True, guests = False}, + collabora = CellsCollaboraStatus {enabled = False}, + publicLinks = + CellsPublicLinks + { enableFiles = True, + enableFolders = True, + enforcePassword = False, + enforceExpirationMax = 0, + enforceExpirationDefault = 0 + }, + storage = + CellsConfigStorage + { perFileQuotaBytes = NumBytes $ BigIntString 100000000, -- 100MB + recycle = + CellsRecycle + { autoPurgeDays = 30, + disable = False, + allowSkip = False + } + }, + metadata = + CellsMetadata + { namespaces = + CellsNamespaces + { usermetaTags = + CellsUserMetaTags + { defaultValues = [], + allowFreeValues = True + } + } + } + } + +instance (FieldF f) => ToSchema (CellsConfigB Covered f) where + schema = + object "CellsConfig" $ + CellsConfig + <$> channels .= fieldF "channels" schema + <*> groups .= fieldF "groups" schema + <*> one2one .= fieldF "one2one" schema + <*> users .= fieldF "users" schema + <*> (.collabora) .= fieldF "collabora" schema + <*> publicLinks .= fieldF "publicLinks" schema + <*> (.storage) .= fieldF "storage" schema + <*> metadata .= fieldF "metadata" schema + +instance ToSchema (Versioned V13 CellsConfig) where + schema = object "CellsConfigV13" objectSchema + +instance ToObjectSchema (Versioned V13 CellsConfig) where + objectSchema = pure $ Versioned def instance Default (LockableFeature CellsConfig) where def = defLockedFeature +instance ToObjectSchema CellsConfig where + objectSchema = field "config" schema + instance IsFeatureConfig CellsConfig where type FeatureSymbol CellsConfig = "cells" featureSingleton = FeatureSingletonCellsConfig - objectSchema = pure CellsConfig ---------------------------------------------------------------------- -- Cells Internal @@ -1499,32 +1783,24 @@ newtype CellsBackend = CellsBackend instance ToSchema CellsBackend where schema = object "CellsBackend" $ CellsBackend <$> url .= field "url" schema -newtype NumBytes = NumBytes {unNumBytes :: Int64} +newtype NumBytes = NumBytes {unNumBytes :: BigIntString} deriving newtype (Show, Eq) deriving (ToJSON, FromJSON, S.ToSchema) via Schema NumBytes instance Arbitrary NumBytes where - arbitrary = NumBytes <$> choose (0 :: Int64, maxBound) + arbitrary = NumBytes . BigIntString <$> choose (0 :: Integer, 99999999999999999999999999) instance ToSchema NumBytes where - schema = toText .= (NumBytes <$> numBytesSchema) - where - toText :: NumBytes -> Text - toText = T.pack . show . unNumBytes - - numBytesSchema :: ValueSchemaP NamedSwaggerDoc Text Int64 - numBytesSchema = schema `withParser` parseNumBytes - where - parseNumBytes :: Text -> A.Parser Int64 - parseNumBytes txt = do - (n, rest) <- either fail pure (TR.decimal txt :: Either String (Integer, Text)) - unless (T.null rest) $ - fail "numBytes must be an integer string without decimals" - when (n <= 0) $ - fail "numBytes must be positive" - when (n > toInteger (maxBound @Int64)) $ - fail "numBytes must fit into Int64" - pure (fromInteger n) + schema = + NumBytes + <$> unNumBytes + .= withParser + schema + ( \n@(BigIntString i) -> do + when (i < 0) $ + fail "numBytes must be non-negative" + pure n + ) newtype CellsStorage = CellsStorage { teamQuotaBytes :: NumBytes @@ -1571,7 +1847,7 @@ instance Default CellsInternalConfig where CellsInternalConfig { backend = CellsBackend $ HttpsUrl [URI.QQ.uri|https://cells-beta.wire.com|], collabora = CellsCollabora Cool, - storage = CellsStorage $ NumBytes 1000000000000 -- 1 TB + storage = CellsStorage $ NumBytes $ BigIntString 1000000000000 -- 1 TB } instance (FieldF f) => ToSchema (CellsInternalConfigB Covered f) where @@ -1579,16 +1855,18 @@ instance (FieldF f) => ToSchema (CellsInternalConfigB Covered f) where object "CellsInternalConfig" $ CellsInternalConfig <$> backend .= fieldF "backend" schema - <*> collabora .= fieldF "collabora" schema - <*> storage .= fieldF "storage" schema + <*> (.collabora) .= fieldF "collabora" schema + <*> (.storage) .= fieldF "storage" schema instance Default (LockableFeature CellsInternalConfig) where def = defUnlockedFeature +instance ToObjectSchema CellsInternalConfig where + objectSchema = field "config" schema + instance IsFeatureConfig CellsInternalConfig where type FeatureSymbol CellsInternalConfig = "cellsInternal" featureSingleton = FeatureSingletonCellsInternalConfig - objectSchema = field "config" schema -------------------------------------------------------------------------------- -- Allowed Global Operations feature @@ -1618,11 +1896,13 @@ instance ToSchema AllowedGlobalOperationsConfig where instance Default (LockableFeature AllowedGlobalOperationsConfig) where def = defLockedFeature {status = FeatureStatusEnabled} +instance ToObjectSchema AllowedGlobalOperationsConfig where + objectSchema = field "config" schema + instance IsFeatureConfig AllowedGlobalOperationsConfig where type FeatureSymbol AllowedGlobalOperationsConfig = "allowedGlobalOperations" featureSingleton = FeatureSingletonAllowedGlobalOperationsConfig - objectSchema = field "config" schema -------------------------------------------------------------------------------- -- Asset Audit Log feature @@ -1648,10 +1928,12 @@ instance Default AssetAuditLogConfig where instance Default (LockableFeature AssetAuditLogConfig) where def = defLockedFeature +instance ToObjectSchema AssetAuditLogConfig where + objectSchema = pure AssetAuditLogConfig + instance IsFeatureConfig AssetAuditLogConfig where type FeatureSymbol AssetAuditLogConfig = "assetAuditLog" featureSingleton = FeatureSingletonAssetAuditLogConfig - objectSchema = pure AssetAuditLogConfig -------------------------------------------------------------------------------- -- ConsumableNotifications feature @@ -1669,10 +1951,12 @@ instance ToSchema ConsumableNotificationsConfig where instance Default (LockableFeature ConsumableNotificationsConfig) where def = defLockedFeature +instance ToObjectSchema ConsumableNotificationsConfig where + objectSchema = pure ConsumableNotificationsConfig + instance IsFeatureConfig ConsumableNotificationsConfig where type FeatureSymbol ConsumableNotificationsConfig = "consumableNotifications" featureSingleton = FeatureSingletonConsumableNotificationsConfig - objectSchema = pure ConsumableNotificationsConfig -------------------------------------------------------------------------------- -- Chat Bubbles Feature @@ -1689,12 +1973,13 @@ instance ToSchema ChatBubblesConfig where instance Default (LockableFeature ChatBubblesConfig) where def = defLockedFeature +instance ToObjectSchema ChatBubblesConfig where + objectSchema = pure ChatBubblesConfig + instance IsFeatureConfig ChatBubblesConfig where type FeatureSymbol ChatBubblesConfig = "chatBubbles" featureSingleton = FeatureSingletonChatBubblesConfig - objectSchema = pure ChatBubblesConfig - ------------------------------------------------------------------------------- -- Apps Feature @@ -1710,12 +1995,13 @@ instance ToSchema AppsConfig where instance Default (LockableFeature AppsConfig) where def = defLockedFeature +instance ToObjectSchema AppsConfig where + objectSchema = pure AppsConfig + instance IsFeatureConfig AppsConfig where type FeatureSymbol AppsConfig = "apps" featureSingleton = FeatureSingletonAppsConfig - objectSchema = pure AppsConfig - -------------------------------------------------------------------------------- -- "Simplified User Connection Request QR Code" Feature -- @@ -1734,12 +2020,13 @@ instance ToSchema SimplifiedUserConnectionRequestQRCodeConfig where instance Default (LockableFeature SimplifiedUserConnectionRequestQRCodeConfig) where def = defUnlockedFeature +instance ToObjectSchema SimplifiedUserConnectionRequestQRCodeConfig where + objectSchema = pure SimplifiedUserConnectionRequestQRCodeConfig + instance IsFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig where type FeatureSymbol SimplifiedUserConnectionRequestQRCodeConfig = "simplifiedUserConnectionRequestQRCode" featureSingleton = FeatureSingletonSimplifiedUserConnectionRequestQRCodeConfig - objectSchema = pure SimplifiedUserConnectionRequestQRCodeConfig - -------------------------------------------------------------------------------- -- Stealth Users @@ -1755,12 +2042,13 @@ instance ToSchema StealthUsersConfig where instance Default (LockableFeature StealthUsersConfig) where def = defLockedFeature +instance ToObjectSchema StealthUsersConfig where + objectSchema = pure StealthUsersConfig + instance IsFeatureConfig StealthUsersConfig where type FeatureSymbol StealthUsersConfig = "stealthUsers" featureSingleton = FeatureSingletonStealthUsersConfig - objectSchema = pure StealthUsersConfig - --------------------------------------------------------------------------------- -- FeatureStatus diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 28c9463abf3..4af0f52d434 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -50,7 +50,7 @@ import Test.QuickCheck import Wire.API.EnterpriseLogin import Wire.API.Federation.Error import Wire.API.Team.Collaborator -import Wire.API.Team.Feature +import Wire.API.Team.Feature (FeatureStatus (..), LockStatus (..), LockableFeature (..), MlsE2EIdConfig, npUpdate) import Wire.API.Team.Member import Wire.API.Team.Permission import Wire.API.Team.Role diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 30d103eded1..ec5f20aeb33 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -163,6 +163,38 @@ settings: defaults: status: enabled lockStatus: unlocked + config: + channels: + enabled: true + default: enabled + groups: + enabled: true + default: enabled + one2one: + enabled: true + default: enabled + users: + externals: true + guests: false + collabora: + enabled: false + publicLinks: + enableFiles: true + enableFolders: true + enforcePassword: false + enforceExpirationMax: 0 + enforceExpirationDefault: 0 + storage: + perFileQuotaBytes: "100000000" + recycle: + autoPurgeDays: 30 + disable: false + allowSkip: false + metadata: + namespaces: + usermetaTags: + defaultValues: [] + allowFreeValues: true cellsInternal: defaults: status: enabled diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 9cb5266deda..ccfd15e38d7 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -67,7 +67,9 @@ featureAPI = <@> deprecatedFeatureAPI <@> mkNamedAPI @'("get", DomainRegistrationConfig) getFeature <@> featureAPIGetPut - <@> featureAPIGetPut + <@> mkNamedAPI @'("get", CellsConfig) getFeature + <@> mkNamedAPI @"put-CellsConfig@v13" setFeature + <@> mkNamedAPI @'("put", CellsConfig) setFeature <@> mkNamedAPI @'("get", AllowedGlobalOperationsConfig) getFeature <@> mkNamedAPI @'("get", AssetAuditLogConfig) getFeature <@> mkNamedAPI @'("get", ConsumableNotificationsConfig) getFeature diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 91a78d0ee48..a741892235c 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -173,7 +173,7 @@ sitemap' = :<|> Named @"get-route-enforce-file-download-location" (mkFeatureGetRoute @EnforceFileDownloadLocationConfig) :<|> Named @"put-route-enforce-file-download-location" (mkFeaturePutRoute @EnforceFileDownloadLocationConfig) :<|> Named @"get-route-cells" (mkFeatureGetRoute @CellsConfig) - :<|> Named @"put-route-cells" (mkFeatureStatusPutRoute @CellsConfig) + :<|> Named @"put-route-cells" (mkFeaturePutRoute @CellsConfig) :<|> Named @"get-route-cells-internal" (mkFeatureGetRoute @CellsInternalConfig) :<|> Named @"put-route-cells-internal" (mkFeaturePutRoute @CellsInternalConfig) :<|> Named @"get-route-guest-links" (mkFeatureGetRoute @GuestLinksConfig) diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 4ca6a56932d..b08a9f9c644 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -308,7 +308,7 @@ type SternAPI = :> MkFeaturePutRoute EnforceFileDownloadLocationConfig ) :<|> Named "get-route-cells" (MkFeatureGetRoute CellsConfig) - :<|> Named "put-route-cells" (MkFeatureStatusPutRoute CellsConfig) + :<|> Named "put-route-cells" (MkFeaturePutRoute CellsConfig) :<|> Named "get-route-cells-internal" (MkFeatureGetRoute CellsInternalConfig) :<|> Named "put-route-cells-internal" (MkFeaturePutRoute CellsInternalConfig) :<|> Named "get-route-guest-links" (MkFeatureGetRoute GuestLinksConfig) diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index e82e73711e9..903337fdc81 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -33,6 +33,7 @@ import Data.ByteString.Conversion import Data.Default import Data.Handle import Data.Id +import Data.Json.Util (BigIntString (..)) import Data.Misc (HttpsUrl) import Data.Range (unsafeRange) import Data.Schema @@ -105,6 +106,7 @@ tests s = test s "i/domain-registration" testDomainRegistration, test s "GET /teams/:tid/features/domainRegistration" $ testFeatureStatus @DomainRegistrationConfig, test s "PUT /teams/:tid/features/domainRegistration{,'?lockOrUnlock'}" $ testFeatureStatusWithLock @DomainRegistrationConfig, + test s "/teams/:tid/features/cells" testCellsConfigRoutes, test s "/teams/:tid/features/channels" $ testLockedFeatureConfig @ChannelsConfig, test s "PUT /teams/:tid/features/channels{,'?lockOrUnlock'}" $ testLockStatus @ChannelsConfig, test s "PUT /teams/:tid/features/digitalSignatures{,'?lockOrUnlock'}" $ testLockStatus @DigitalSignaturesConfig, @@ -329,6 +331,63 @@ testFeatureConfig = do cfg' <- getFeatureConfig @cfg tid liftIO $ cfg'.status @?= newStatus +testCellsConfigRoutes :: TestM () +testCellsConfigRoutes = do + (_, tid, _) <- createTeamWithNMembers 1 + cfg <- getFeatureConfig @CellsConfig tid + -- at the time of writing the galley.integration.yaml has the feature enabled and unlocked + liftIO $ cfg @?= def {status = FeatureStatusEnabled, lockStatus = LockStatusUnlocked} + + putFeatureStatusLock @CellsConfig tid LockStatusUnlocked Nothing !!! const 200 === statusCode + + let updatedConfig :: LockableFeature CellsConfig + updatedConfig = + LockableFeature + { status = FeatureStatusEnabled, + lockStatus = LockStatusUnlocked, + config = + CellsConfig + { channels = CellsProperty {enabled = False, default_ = Enforced}, + groups = CellsProperty {enabled = True, default_ = Disabled}, + one2one = CellsProperty {enabled = True, default_ = Enabled}, + users = CellsUsers {externals = False, guests = True}, + collabora = CellsCollaboraStatus {enabled = True}, + publicLinks = + CellsPublicLinks + { enableFiles = True, + enableFolders = False, + enforcePassword = True, + enforceExpirationMax = 86400, + enforceExpirationDefault = 3600 + }, + storage = + CellsConfigStorage + { perFileQuotaBytes = NumBytes (BigIntString 2000000000), + recycle = + CellsRecycle + { autoPurgeDays = 14, + disable = False, + allowSkip = True + } + }, + metadata = + CellsMetadata + { namespaces = + CellsNamespaces + { usermetaTags = + CellsUserMetaTags + { defaultValues = ["default-tag"], + allowFreeValues = False + } + } + } + } + } + + putFeatureConfig @CellsConfig tid updatedConfig !!! const 200 === statusCode + cfg' <- getFeatureConfig @CellsConfig tid + liftIO $ cfg' @?= updatedConfig + testCellsInternalConfig :: TestM () testCellsInternalConfig = do (_, tid, _) <- createTeamWithNMembers 1 @@ -341,7 +400,7 @@ testCellsInternalConfig = do cfg.config { backend = CellsBackend newBackend, collabora = CellsCollabora Cool, - storage = CellsStorage (NumBytes 2000000000000) + storage = CellsStorage (NumBytes (BigIntString 2000000000000)) } } :: LockableFeature CellsInternalConfig From ac980f0d94758c21b92e5ce9de8ed2666e2826ed Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 19 Dec 2025 11:01:23 +0100 Subject: [PATCH 31/60] WPB-21964: introduce Wire Meetings feature flags --- changelog.d/2-features/WPB-21964 | 3 + charts/galley/templates/configmap.yaml | 8 +++ charts/galley/values.yaml | 8 +++ .../dockerephemeral/federation-v0/galley.yaml | 8 +++ .../dockerephemeral/federation-v1/galley.yaml | 8 +++ .../dockerephemeral/federation-v2/galley.yaml | 8 +++ .../src/developer/reference/config-options.md | 48 +++++++++++++++ integration/integration.cabal | 2 + integration/test/Test/FeatureFlags/Meeting.hs | 31 ++++++++++ .../test/Test/FeatureFlags/MeetingPremium.hs | 31 ++++++++++ integration/test/Test/FeatureFlags/Util.hs | 6 +- libs/galley-types/src/Galley/Types/Teams.hs | 14 +++++ .../src/Wire/API/Routes/Internal/Galley.hs | 2 + .../Wire/API/Routes/Public/Galley/Feature.hs | 2 + libs/wire-api/src/Wire/API/Team/Feature.hs | 58 ++++++++++++++++++- services/galley/galley.integration.yaml | 8 +++ services/galley/src/Galley/API/Internal.hs | 4 ++ .../galley/src/Galley/API/Public/Feature.hs | 2 + .../galley/src/Galley/API/Teams/Features.hs | 4 ++ .../src/Galley/API/Teams/Features/Get.hs | 4 ++ tools/stern/src/Stern/API.hs | 6 ++ tools/stern/src/Stern/API/Routes.hs | 6 ++ 22 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2-features/WPB-21964 create mode 100644 integration/test/Test/FeatureFlags/Meeting.hs create mode 100644 integration/test/Test/FeatureFlags/MeetingPremium.hs diff --git a/changelog.d/2-features/WPB-21964 b/changelog.d/2-features/WPB-21964 new file mode 100644 index 00000000000..13d16c8ddfa --- /dev/null +++ b/changelog.d/2-features/WPB-21964 @@ -0,0 +1,3 @@ +Add meetingPremium feature flag to distinguish premium teams from trial teams. Meetings created by premium team members are marked as non-trial. Public endpoints: GET/PUT /teams/:tid/features/meetingPremium. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetingPremium and lock status management. + +Add meeting feature flag to control access to the meetings API. When disabled, all meetings endpoints return 403 Forbidden. The feature is enabled and unlocked by default. Public endpoints: GET/PUT /teams/:tid/features/meeting. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meeting and lock status management. diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 5c9072a5169..4d64c9f11f6 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -218,5 +218,13 @@ data: stealthUsers: {{- toYaml .settings.featureFlags.stealthUsers | nindent 10 }} {{- end }} + {{- if .settings.featureFlags.meeting }} + meeting: + {{- toYaml .settings.featureFlags.meeting | nindent 10 }} + {{- end }} + {{- if .settings.featureFlags.meetingPremium }} + meetingPremium: + {{- toYaml .settings.featureFlags.meetingPremium | nindent 10 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index d0ed584bf67..8e328671d49 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -288,6 +288,14 @@ config: defaults: status: disabled lockStatus: locked + meeting: + defaults: + status: enabled + lockStatus: unlocked + meetingPremium: + defaults: + status: enabled + lockStatus: unlocked aws: region: "eu-west-1" proxy: {} diff --git a/deploy/dockerephemeral/federation-v0/galley.yaml b/deploy/dockerephemeral/federation-v0/galley.yaml index ab2644a8ef5..5be62c125af 100644 --- a/deploy/dockerephemeral/federation-v0/galley.yaml +++ b/deploy/dockerephemeral/federation-v0/galley.yaml @@ -83,6 +83,14 @@ settings: verificationExpiration: 86400 acmeDiscoveryUrl: null lockStatus: unlocked + meeting: + defaults: + status: enabled + lockStatus: unlocked + meetingPremium: + defaults: + status: enabled + lockStatus: unlocked logLevel: Warn logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/galley.yaml b/deploy/dockerephemeral/federation-v1/galley.yaml index f272536260c..d4a5070163e 100644 --- a/deploy/dockerephemeral/federation-v1/galley.yaml +++ b/deploy/dockerephemeral/federation-v1/galley.yaml @@ -97,6 +97,14 @@ settings: limitedEventFanout: defaults: status: disabled + meeting: + defaults: + status: enabled + lockStatus: unlocked + meetingPremium: + defaults: + status: enabled + lockStatus: unlocked logLevel: Warn logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v2/galley.yaml b/deploy/dockerephemeral/federation-v2/galley.yaml index 94d268ae0e8..4b8257f9867 100644 --- a/deploy/dockerephemeral/federation-v2/galley.yaml +++ b/deploy/dockerephemeral/federation-v2/galley.yaml @@ -146,6 +146,14 @@ settings: defaults: status: enabled lockStatus: unlocked + meeting: + defaults: + status: enabled + lockStatus: unlocked + meetingPremium: + defaults: + status: enabled + lockStatus: unlocked logLevel: Warn logNetStrings: false diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index e6c126e997d..d8a59945ad3 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -234,6 +234,54 @@ The `conferenceCalling` section is optional in `featureFlags`. If it is omitted See also: conference falling for personal accounts (below). +### Meetings + +The `meeting` feature flag controls whether a user can initiate a meeting. It is enabled and unlocked by default. If you want a different configuration, use the following syntax: + +```yaml +meeting: + defaults: + status: disabled|enabled + lockStatus: locked|unlocked +``` + +These are all the possible combinations of `status` and `lockStatus`: + +| `status` | `lockStatus` | | +| ---------- | ------------ | ------------------------------------------------- | +| `enabled` | `locked` | Feature enabled, cannot be disabled by team admin | +| `enabled` | `unlocked` | Feature enabled, can be disabled by team admin | +| `disabled` | `locked` | Feature disabled, cannot be enabled by team admin | +| `disabled` | `unlocked` | Feature disabled, can be enabled by team admin | + +The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/meeting/(un)?locked`). + +The feature status for individual teams can be changed via the public API (if the feature is unlocked). + +### Meeting Premium + +The `meetingPremium` feature flag controls whether a team has premium meeting features. When enabled, meetings created by team members are not marked as trial. When disabled, meetings are trial and limited to 25 minutes. It is enabled and unlocked by default. If you want a different configuration, use the following syntax: + +```yaml +meetingPremium: + defaults: + status: disabled|enabled + lockStatus: locked|unlocked +``` + +These are all the possible combinations of `status` and `lockStatus`: + +| `status` | `lockStatus` | | +| ---------- | ------------ | ------------------------------------------------- | +| `enabled` | `locked` | Feature enabled, cannot be disabled by team admin | +| `enabled` | `unlocked` | Feature enabled, can be disabled by team admin | +| `disabled` | `locked` | Feature disabled, cannot be enabled by team admin | +| `disabled` | `unlocked` | Feature disabled, can be enabled by team admin | + +The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/meetingPremium/(un)?locked`). + +The feature status for individual teams can be changed via the public API (if the feature is unlocked). + ### File Sharing File sharing is enabled and unlocked by default. If you want a different configuration, use the following syntax: diff --git a/integration/integration.cabal b/integration/integration.cabal index 03df02b4383..555ca6a7f1b 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -153,6 +153,8 @@ library Test.FeatureFlags.FileSharing Test.FeatureFlags.GuestLinks Test.FeatureFlags.LegalHold + Test.FeatureFlags.Meeting + Test.FeatureFlags.MeetingPremium Test.FeatureFlags.Mls Test.FeatureFlags.MlsE2EId Test.FeatureFlags.MlsMigration diff --git a/integration/test/Test/FeatureFlags/Meeting.hs b/integration/test/Test/FeatureFlags/Meeting.hs new file mode 100644 index 00000000000..afd828871ea --- /dev/null +++ b/integration/test/Test/FeatureFlags/Meeting.hs @@ -0,0 +1,31 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 Test.FeatureFlags.Meeting where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchMeeting :: (HasCallStack) => App () +testPatchMeeting = checkPatch OwnDomain "meeting" disabled + +testMeeting :: (HasCallStack) => APIAccess -> App () +testMeeting access = + mkFeatureTests "meeting" + & addUpdate disabled + & addUpdate enabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/MeetingPremium.hs b/integration/test/Test/FeatureFlags/MeetingPremium.hs new file mode 100644 index 00000000000..fcf26876c30 --- /dev/null +++ b/integration/test/Test/FeatureFlags/MeetingPremium.hs @@ -0,0 +1,31 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 Test.FeatureFlags.MeetingPremium where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchMeetingPremium :: (HasCallStack) => App () +testPatchMeetingPremium = checkPatch OwnDomain "meetingPremium" disabled + +testMeetingPremium :: (HasCallStack) => APIAccess -> App () +testMeetingPremium access = + mkFeatureTests "meetingPremium" + & addUpdate disabled + & addUpdate enabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 3c7f617d8ca..21b17059eca 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -240,7 +240,9 @@ defAllFeatures = "collabora" .= object ["edition" .= "COOL"], "storage" .= object ["teamQuotaBytes" .= "1000000000000"] ] - ] + ], + "meeting" .= enabled, + "meetingPremium" .= enabled ] hasExplicitLockStatus :: String -> Bool @@ -252,6 +254,8 @@ hasExplicitLockStatus "sndFactorPasswordChallenge" = True hasExplicitLockStatus "outlookCalIntegration" = True hasExplicitLockStatus "enforceFileDownloadLocation" = True hasExplicitLockStatus "domainRegistration" = True +hasExplicitLockStatus "meeting" = True +hasExplicitLockStatus "meetingPremium" = True hasExplicitLockStatus _ = False checkFeature :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index d63df1d3f79..ef49bcdb26b 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -330,6 +330,20 @@ newtype instance FeatureDefaults StealthUsersConfig deriving (FromJSON) via Defaults (LockableFeature StealthUsersConfig) deriving (ParseFeatureDefaults) via OptionalField StealthUsersConfig +newtype instance FeatureDefaults MeetingConfig + = MeetingDefaults (LockableFeature MeetingConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MeetingConfig) + deriving (ParseFeatureDefaults) via OptionalField MeetingConfig + +newtype instance FeatureDefaults MeetingPremiumConfig + = MeetingPremiumDefaults (LockableFeature MeetingPremiumConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MeetingPremiumConfig) + deriving (ParseFeatureDefaults) via OptionalField MeetingPremiumConfig + featureKey :: forall cfg. (IsFeatureConfig cfg) => Key.Key featureKey = Key.fromText $ featureName @cfg diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index 6dedd4ccdd6..98d688df0a0 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -94,6 +94,8 @@ type IFeatureAPI = :<|> IFeatureStatusLockStatusPut AppsConfig :<|> IFeatureStatusLockStatusPut SimplifiedUserConnectionRequestQRCodeConfig :<|> IFeatureStatusLockStatusPut StealthUsersConfig + :<|> IFeatureStatusLockStatusPut MeetingConfig + :<|> IFeatureStatusLockStatusPut MeetingPremiumConfig -- all feature configs :<|> Named "feature-configs-internal" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index d0c02b34126..9d3b3b89c49 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -78,6 +78,8 @@ type FeatureAPI = :<|> FeatureAPIGet SimplifiedUserConnectionRequestQRCodeConfig :<|> FeatureAPIGet StealthUsersConfig :<|> FeatureAPIGet CellsInternalConfig + :<|> FeatureAPIGetPut MeetingConfig + :<|> FeatureAPIGetPut MeetingPremiumConfig type VersionedFeatureAPIPut named reqBodyVersion cfg = Named diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 0e323507d49..7f615cf4258 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -111,6 +111,8 @@ module Wire.API.Team.Feature AppsConfig (..), SimplifiedUserConnectionRequestQRCodeConfig (..), StealthUsersConfig (..), + MeetingConfig (..), + MeetingPremiumConfig (..), Features, AllFeatures, NpProject (..), @@ -275,6 +277,8 @@ data FeatureSingleton cfg where FeatureSingletonAssetAuditLogConfig :: FeatureSingleton AssetAuditLogConfig FeatureSingletonStealthUsersConfig :: FeatureSingleton StealthUsersConfig FeatureSingletonCellsInternalConfig :: FeatureSingleton CellsInternalConfig + FeatureSingletonMeetingConfig :: FeatureSingleton MeetingConfig + FeatureSingletonMeetingPremiumConfig :: FeatureSingleton MeetingPremiumConfig type family DeprecatedFeatureName (v :: Version) (cfg :: Type) :: Symbol @@ -2049,6 +2053,56 @@ instance IsFeatureConfig StealthUsersConfig where type FeatureSymbol StealthUsersConfig = "stealthUsers" featureSingleton = FeatureSingletonStealthUsersConfig +-------------------------------------------------------------------------------- +-- Meeting Feature +-- +-- Controls whether meetings functionality is available. When enabled, users can +-- create and manage meetings. When disabled, meeting endpoints are not accessible. + +data MeetingConfig = MeetingConfig + deriving (Eq, Show, Generic, GSOP.Generic) + deriving (Arbitrary) via (GenericUniform MeetingConfig) + deriving (RenderableSymbol) via (RenderableTypeName MeetingConfig) + deriving (ParseDbFeature, Default) via TrivialFeature MeetingConfig + +instance ToSchema MeetingConfig where + schema = object "MeetingConfig" objectSchema + +instance Default (LockableFeature MeetingConfig) where + def = defUnlockedFeature + +instance IsFeatureConfig MeetingConfig where + type FeatureSymbol MeetingConfig = "meeting" + featureSingleton = FeatureSingletonMeetingConfig + +instance ToObjectSchema MeetingConfig where + objectSchema = pure MeetingConfig + +-------------------------------------------------------------------------------- +-- MeetingPremium Feature +-- +-- Indicates whether a team has premium meeting features. When enabled, meetings +-- created by team members are not marked as trial. When disabled, meetings are trial. + +data MeetingPremiumConfig = MeetingPremiumConfig + deriving (Eq, Show, Generic, GSOP.Generic) + deriving (Arbitrary) via (GenericUniform MeetingPremiumConfig) + deriving (RenderableSymbol) via (RenderableTypeName MeetingPremiumConfig) + deriving (ParseDbFeature, Default) via TrivialFeature MeetingPremiumConfig + +instance ToSchema MeetingPremiumConfig where + schema = object "MeetingPremiumConfig" objectSchema + +instance Default (LockableFeature MeetingPremiumConfig) where + def = defUnlockedFeature + +instance IsFeatureConfig MeetingPremiumConfig where + type FeatureSymbol MeetingPremiumConfig = "meetingPremium" + featureSingleton = FeatureSingletonMeetingPremiumConfig + +instance ToObjectSchema MeetingPremiumConfig where + objectSchema = pure MeetingPremiumConfig + --------------------------------------------------------------------------------- -- FeatureStatus @@ -2142,7 +2196,9 @@ type Features = SimplifiedUserConnectionRequestQRCodeConfig, AssetAuditLogConfig, StealthUsersConfig, - CellsInternalConfig + CellsInternalConfig, + MeetingConfig, + MeetingPremiumConfig ] -- | list of available features as a record diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index ec5f20aeb33..3170e3cee3a 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -228,6 +228,14 @@ settings: defaults: status: disabled lockStatus: locked + meeting: + defaults: + status: enabled + lockStatus: unlocked + meetingPremium: + defaults: + status: enabled + lockStatus: unlocked logLevel: Warn logNetStrings: false diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 5e738062b26..356580e758d 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -292,6 +292,8 @@ allFeaturesAPI = <@> featureAPI1Get <@> featureAPI1Full <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full featureAPI :: API IFeatureAPI GalleyEffects featureAPI = @@ -315,6 +317,8 @@ featureAPI = <@> mkNamedAPI @'("ilock", AppsConfig) (updateLockStatus @AppsConfig) <@> mkNamedAPI @'("ilock", SimplifiedUserConnectionRequestQRCodeConfig) (updateLockStatus @SimplifiedUserConnectionRequestQRCodeConfig) <@> mkNamedAPI @'("ilock", StealthUsersConfig) (updateLockStatus @StealthUsersConfig) + <@> mkNamedAPI @'("ilock", MeetingConfig) (updateLockStatus @MeetingConfig) + <@> mkNamedAPI @'("ilock", MeetingPremiumConfig) (updateLockStatus @MeetingPremiumConfig) -- all features <@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser) diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index ccfd15e38d7..dff22ef12b7 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -78,6 +78,8 @@ featureAPI = <@> mkNamedAPI @'("get", SimplifiedUserConnectionRequestQRCodeConfig) getFeature <@> mkNamedAPI @'("get", StealthUsersConfig) getFeature <@> mkNamedAPI @'("get", CellsInternalConfig) getFeature + <@> featureAPIGetPut @MeetingConfig + <@> featureAPIGetPut @MeetingPremiumConfig deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects deprecatedFeatureConfigAPI = diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 72da3558e03..00befc792b1 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -485,3 +485,7 @@ instance SetFeatureConfig AppsConfig instance SetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig instance SetFeatureConfig StealthUsersConfig + +instance SetFeatureConfig MeetingConfig + +instance SetFeatureConfig MeetingPremiumConfig diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index ff817e01696..a990541e236 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -428,6 +428,10 @@ instance GetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig instance GetFeatureConfig StealthUsersConfig +instance GetFeatureConfig MeetingConfig + +instance GetFeatureConfig MeetingPremiumConfig + -- | If second factor auth is enabled, make sure that end-points that don't support it, but -- should, are blocked completely. (This is a workaround until we have 2FA for those -- end-points as well.) diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index a741892235c..01fb0f364d8 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -192,6 +192,10 @@ sitemap' = :<|> Named @"put-route-apps-config" (mkFeatureStatusPutRoute @AppsConfig) :<|> Named @"get-route-stealth-users-config" (mkFeatureGetRoute @StealthUsersConfig) :<|> Named @"put-route-stealth-users-config" (mkFeatureStatusPutRoute @StealthUsersConfig) + :<|> Named @"get-route-meeting-config" (mkFeatureGetRoute @MeetingConfig) + :<|> Named @"put-route-meeting-config" (mkFeatureStatusPutRoute @MeetingConfig) + :<|> Named @"get-route-meeting-premium-config" (mkFeatureGetRoute @MeetingPremiumConfig) + :<|> Named @"put-route-meeting-premium-config" (mkFeatureStatusPutRoute @MeetingPremiumConfig) :<|> Named @"get-team-invoice" getTeamInvoice :<|> Named @"get-team-billing-info" getTeamBillingInfo :<|> Named @"put-team-billing-info" updateTeamBillingInfo @@ -226,6 +230,8 @@ sitemap' = :<|> Named @"lock-unlock-route-consumable-notifications-config" (mkFeatureLockUnlockRoute @ConsumableNotificationsConfig) :<|> Named @"lock-unlock-route-chat-bubbles-config" (mkFeatureLockUnlockRoute @ChatBubblesConfig) :<|> Named @"lock-unlock-route-apps-config" (mkFeatureLockUnlockRoute @AppsConfig) + :<|> Named @"lock-unlock-route-meeting-config" (mkFeatureLockUnlockRoute @MeetingConfig) + :<|> Named @"lock-unlock-route-meeting-premium-config" (mkFeatureLockUnlockRoute @MeetingPremiumConfig) sitemapInternal :: Servant.Server SternAPIInternal sitemapInternal = diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index b08a9f9c644..905cd65ebd4 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -327,6 +327,10 @@ type SternAPI = :<|> Named "put-route-apps-config" (MkFeatureStatusPutRoute AppsConfig) :<|> Named "get-route-stealth-users-config" (MkFeatureGetRoute StealthUsersConfig) :<|> Named "put-route-stealth-users-config" (MkFeatureStatusPutRoute StealthUsersConfig) + :<|> Named "get-route-meeting-config" (MkFeatureGetRoute MeetingConfig) + :<|> Named "put-route-meeting-config" (MkFeatureStatusPutRoute MeetingConfig) + :<|> Named "get-route-meeting-premium-config" (MkFeatureGetRoute MeetingPremiumConfig) + :<|> Named "put-route-meeting-premium-config" (MkFeatureStatusPutRoute MeetingPremiumConfig) :<|> Named "get-team-invoice" ( Summary "Get a specific invoice by Number" @@ -478,6 +482,8 @@ type SternAPI = :<|> Named "lock-unlock-route-consumable-notifications-config" (MkFeatureLockUnlockRoute ConsumableNotificationsConfig) :<|> Named "lock-unlock-route-chat-bubbles-config" (MkFeatureLockUnlockRoute ChatBubblesConfig) :<|> Named "lock-unlock-route-apps-config" (MkFeatureLockUnlockRoute AppsConfig) + :<|> Named "lock-unlock-route-meeting-config" (MkFeatureLockUnlockRoute MeetingConfig) + :<|> Named "lock-unlock-route-meeting-premium-config" (MkFeatureLockUnlockRoute MeetingPremiumConfig) ------------------------------------------------------------------------------- -- Swagger From d27be23e1acb8a058a49a3a1fb8ed86024f54ff1 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 19 Dec 2025 11:06:12 +0100 Subject: [PATCH 32/60] Revert "WPB-21964: introduce Wire Meetings feature flags" This reverts commit ac980f0d94758c21b92e5ce9de8ed2666e2826ed. --- changelog.d/2-features/WPB-21964 | 3 - charts/galley/templates/configmap.yaml | 8 --- charts/galley/values.yaml | 8 --- .../dockerephemeral/federation-v0/galley.yaml | 8 --- .../dockerephemeral/federation-v1/galley.yaml | 8 --- .../dockerephemeral/federation-v2/galley.yaml | 8 --- .../src/developer/reference/config-options.md | 48 --------------- integration/integration.cabal | 2 - integration/test/Test/FeatureFlags/Meeting.hs | 31 ---------- .../test/Test/FeatureFlags/MeetingPremium.hs | 31 ---------- integration/test/Test/FeatureFlags/Util.hs | 6 +- libs/galley-types/src/Galley/Types/Teams.hs | 14 ----- .../src/Wire/API/Routes/Internal/Galley.hs | 2 - .../Wire/API/Routes/Public/Galley/Feature.hs | 2 - libs/wire-api/src/Wire/API/Team/Feature.hs | 58 +------------------ services/galley/galley.integration.yaml | 8 --- services/galley/src/Galley/API/Internal.hs | 4 -- .../galley/src/Galley/API/Public/Feature.hs | 2 - .../galley/src/Galley/API/Teams/Features.hs | 4 -- .../src/Galley/API/Teams/Features/Get.hs | 4 -- tools/stern/src/Stern/API.hs | 6 -- tools/stern/src/Stern/API/Routes.hs | 6 -- 22 files changed, 2 insertions(+), 269 deletions(-) delete mode 100644 changelog.d/2-features/WPB-21964 delete mode 100644 integration/test/Test/FeatureFlags/Meeting.hs delete mode 100644 integration/test/Test/FeatureFlags/MeetingPremium.hs diff --git a/changelog.d/2-features/WPB-21964 b/changelog.d/2-features/WPB-21964 deleted file mode 100644 index 13d16c8ddfa..00000000000 --- a/changelog.d/2-features/WPB-21964 +++ /dev/null @@ -1,3 +0,0 @@ -Add meetingPremium feature flag to distinguish premium teams from trial teams. Meetings created by premium team members are marked as non-trial. Public endpoints: GET/PUT /teams/:tid/features/meetingPremium. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetingPremium and lock status management. - -Add meeting feature flag to control access to the meetings API. When disabled, all meetings endpoints return 403 Forbidden. The feature is enabled and unlocked by default. Public endpoints: GET/PUT /teams/:tid/features/meeting. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meeting and lock status management. diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 4d64c9f11f6..5c9072a5169 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -218,13 +218,5 @@ data: stealthUsers: {{- toYaml .settings.featureFlags.stealthUsers | nindent 10 }} {{- end }} - {{- if .settings.featureFlags.meeting }} - meeting: - {{- toYaml .settings.featureFlags.meeting | nindent 10 }} - {{- end }} - {{- if .settings.featureFlags.meetingPremium }} - meetingPremium: - {{- toYaml .settings.featureFlags.meetingPremium | nindent 10 }} - {{- end }} {{- end }} {{- end }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 8e328671d49..d0ed584bf67 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -288,14 +288,6 @@ config: defaults: status: disabled lockStatus: locked - meeting: - defaults: - status: enabled - lockStatus: unlocked - meetingPremium: - defaults: - status: enabled - lockStatus: unlocked aws: region: "eu-west-1" proxy: {} diff --git a/deploy/dockerephemeral/federation-v0/galley.yaml b/deploy/dockerephemeral/federation-v0/galley.yaml index 5be62c125af..ab2644a8ef5 100644 --- a/deploy/dockerephemeral/federation-v0/galley.yaml +++ b/deploy/dockerephemeral/federation-v0/galley.yaml @@ -83,14 +83,6 @@ settings: verificationExpiration: 86400 acmeDiscoveryUrl: null lockStatus: unlocked - meeting: - defaults: - status: enabled - lockStatus: unlocked - meetingPremium: - defaults: - status: enabled - lockStatus: unlocked logLevel: Warn logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/galley.yaml b/deploy/dockerephemeral/federation-v1/galley.yaml index d4a5070163e..f272536260c 100644 --- a/deploy/dockerephemeral/federation-v1/galley.yaml +++ b/deploy/dockerephemeral/federation-v1/galley.yaml @@ -97,14 +97,6 @@ settings: limitedEventFanout: defaults: status: disabled - meeting: - defaults: - status: enabled - lockStatus: unlocked - meetingPremium: - defaults: - status: enabled - lockStatus: unlocked logLevel: Warn logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v2/galley.yaml b/deploy/dockerephemeral/federation-v2/galley.yaml index 4b8257f9867..94d268ae0e8 100644 --- a/deploy/dockerephemeral/federation-v2/galley.yaml +++ b/deploy/dockerephemeral/federation-v2/galley.yaml @@ -146,14 +146,6 @@ settings: defaults: status: enabled lockStatus: unlocked - meeting: - defaults: - status: enabled - lockStatus: unlocked - meetingPremium: - defaults: - status: enabled - lockStatus: unlocked logLevel: Warn logNetStrings: false diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index d8a59945ad3..e6c126e997d 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -234,54 +234,6 @@ The `conferenceCalling` section is optional in `featureFlags`. If it is omitted See also: conference falling for personal accounts (below). -### Meetings - -The `meeting` feature flag controls whether a user can initiate a meeting. It is enabled and unlocked by default. If you want a different configuration, use the following syntax: - -```yaml -meeting: - defaults: - status: disabled|enabled - lockStatus: locked|unlocked -``` - -These are all the possible combinations of `status` and `lockStatus`: - -| `status` | `lockStatus` | | -| ---------- | ------------ | ------------------------------------------------- | -| `enabled` | `locked` | Feature enabled, cannot be disabled by team admin | -| `enabled` | `unlocked` | Feature enabled, can be disabled by team admin | -| `disabled` | `locked` | Feature disabled, cannot be enabled by team admin | -| `disabled` | `unlocked` | Feature disabled, can be enabled by team admin | - -The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/meeting/(un)?locked`). - -The feature status for individual teams can be changed via the public API (if the feature is unlocked). - -### Meeting Premium - -The `meetingPremium` feature flag controls whether a team has premium meeting features. When enabled, meetings created by team members are not marked as trial. When disabled, meetings are trial and limited to 25 minutes. It is enabled and unlocked by default. If you want a different configuration, use the following syntax: - -```yaml -meetingPremium: - defaults: - status: disabled|enabled - lockStatus: locked|unlocked -``` - -These are all the possible combinations of `status` and `lockStatus`: - -| `status` | `lockStatus` | | -| ---------- | ------------ | ------------------------------------------------- | -| `enabled` | `locked` | Feature enabled, cannot be disabled by team admin | -| `enabled` | `unlocked` | Feature enabled, can be disabled by team admin | -| `disabled` | `locked` | Feature disabled, cannot be enabled by team admin | -| `disabled` | `unlocked` | Feature disabled, can be enabled by team admin | - -The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/meetingPremium/(un)?locked`). - -The feature status for individual teams can be changed via the public API (if the feature is unlocked). - ### File Sharing File sharing is enabled and unlocked by default. If you want a different configuration, use the following syntax: diff --git a/integration/integration.cabal b/integration/integration.cabal index 555ca6a7f1b..03df02b4383 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -153,8 +153,6 @@ library Test.FeatureFlags.FileSharing Test.FeatureFlags.GuestLinks Test.FeatureFlags.LegalHold - Test.FeatureFlags.Meeting - Test.FeatureFlags.MeetingPremium Test.FeatureFlags.Mls Test.FeatureFlags.MlsE2EId Test.FeatureFlags.MlsMigration diff --git a/integration/test/Test/FeatureFlags/Meeting.hs b/integration/test/Test/FeatureFlags/Meeting.hs deleted file mode 100644 index afd828871ea..00000000000 --- a/integration/test/Test/FeatureFlags/Meeting.hs +++ /dev/null @@ -1,31 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 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 Test.FeatureFlags.Meeting where - -import Test.FeatureFlags.Util -import Testlib.Prelude - -testPatchMeeting :: (HasCallStack) => App () -testPatchMeeting = checkPatch OwnDomain "meeting" disabled - -testMeeting :: (HasCallStack) => APIAccess -> App () -testMeeting access = - mkFeatureTests "meeting" - & addUpdate disabled - & addUpdate enabled - & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/MeetingPremium.hs b/integration/test/Test/FeatureFlags/MeetingPremium.hs deleted file mode 100644 index fcf26876c30..00000000000 --- a/integration/test/Test/FeatureFlags/MeetingPremium.hs +++ /dev/null @@ -1,31 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 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 Test.FeatureFlags.MeetingPremium where - -import Test.FeatureFlags.Util -import Testlib.Prelude - -testPatchMeetingPremium :: (HasCallStack) => App () -testPatchMeetingPremium = checkPatch OwnDomain "meetingPremium" disabled - -testMeetingPremium :: (HasCallStack) => APIAccess -> App () -testMeetingPremium access = - mkFeatureTests "meetingPremium" - & addUpdate disabled - & addUpdate enabled - & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 21b17059eca..3c7f617d8ca 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -240,9 +240,7 @@ defAllFeatures = "collabora" .= object ["edition" .= "COOL"], "storage" .= object ["teamQuotaBytes" .= "1000000000000"] ] - ], - "meeting" .= enabled, - "meetingPremium" .= enabled + ] ] hasExplicitLockStatus :: String -> Bool @@ -254,8 +252,6 @@ hasExplicitLockStatus "sndFactorPasswordChallenge" = True hasExplicitLockStatus "outlookCalIntegration" = True hasExplicitLockStatus "enforceFileDownloadLocation" = True hasExplicitLockStatus "domainRegistration" = True -hasExplicitLockStatus "meeting" = True -hasExplicitLockStatus "meetingPremium" = True hasExplicitLockStatus _ = False checkFeature :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index ef49bcdb26b..d63df1d3f79 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -330,20 +330,6 @@ newtype instance FeatureDefaults StealthUsersConfig deriving (FromJSON) via Defaults (LockableFeature StealthUsersConfig) deriving (ParseFeatureDefaults) via OptionalField StealthUsersConfig -newtype instance FeatureDefaults MeetingConfig - = MeetingDefaults (LockableFeature MeetingConfig) - deriving stock (Eq, Show) - deriving newtype (Default, GetFeatureDefaults) - deriving (FromJSON) via Defaults (LockableFeature MeetingConfig) - deriving (ParseFeatureDefaults) via OptionalField MeetingConfig - -newtype instance FeatureDefaults MeetingPremiumConfig - = MeetingPremiumDefaults (LockableFeature MeetingPremiumConfig) - deriving stock (Eq, Show) - deriving newtype (Default, GetFeatureDefaults) - deriving (FromJSON) via Defaults (LockableFeature MeetingPremiumConfig) - deriving (ParseFeatureDefaults) via OptionalField MeetingPremiumConfig - featureKey :: forall cfg. (IsFeatureConfig cfg) => Key.Key featureKey = Key.fromText $ featureName @cfg diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index 98d688df0a0..6dedd4ccdd6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -94,8 +94,6 @@ type IFeatureAPI = :<|> IFeatureStatusLockStatusPut AppsConfig :<|> IFeatureStatusLockStatusPut SimplifiedUserConnectionRequestQRCodeConfig :<|> IFeatureStatusLockStatusPut StealthUsersConfig - :<|> IFeatureStatusLockStatusPut MeetingConfig - :<|> IFeatureStatusLockStatusPut MeetingPremiumConfig -- all feature configs :<|> Named "feature-configs-internal" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index 9d3b3b89c49..d0c02b34126 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -78,8 +78,6 @@ type FeatureAPI = :<|> FeatureAPIGet SimplifiedUserConnectionRequestQRCodeConfig :<|> FeatureAPIGet StealthUsersConfig :<|> FeatureAPIGet CellsInternalConfig - :<|> FeatureAPIGetPut MeetingConfig - :<|> FeatureAPIGetPut MeetingPremiumConfig type VersionedFeatureAPIPut named reqBodyVersion cfg = Named diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 7f615cf4258..0e323507d49 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -111,8 +111,6 @@ module Wire.API.Team.Feature AppsConfig (..), SimplifiedUserConnectionRequestQRCodeConfig (..), StealthUsersConfig (..), - MeetingConfig (..), - MeetingPremiumConfig (..), Features, AllFeatures, NpProject (..), @@ -277,8 +275,6 @@ data FeatureSingleton cfg where FeatureSingletonAssetAuditLogConfig :: FeatureSingleton AssetAuditLogConfig FeatureSingletonStealthUsersConfig :: FeatureSingleton StealthUsersConfig FeatureSingletonCellsInternalConfig :: FeatureSingleton CellsInternalConfig - FeatureSingletonMeetingConfig :: FeatureSingleton MeetingConfig - FeatureSingletonMeetingPremiumConfig :: FeatureSingleton MeetingPremiumConfig type family DeprecatedFeatureName (v :: Version) (cfg :: Type) :: Symbol @@ -2053,56 +2049,6 @@ instance IsFeatureConfig StealthUsersConfig where type FeatureSymbol StealthUsersConfig = "stealthUsers" featureSingleton = FeatureSingletonStealthUsersConfig --------------------------------------------------------------------------------- --- Meeting Feature --- --- Controls whether meetings functionality is available. When enabled, users can --- create and manage meetings. When disabled, meeting endpoints are not accessible. - -data MeetingConfig = MeetingConfig - deriving (Eq, Show, Generic, GSOP.Generic) - deriving (Arbitrary) via (GenericUniform MeetingConfig) - deriving (RenderableSymbol) via (RenderableTypeName MeetingConfig) - deriving (ParseDbFeature, Default) via TrivialFeature MeetingConfig - -instance ToSchema MeetingConfig where - schema = object "MeetingConfig" objectSchema - -instance Default (LockableFeature MeetingConfig) where - def = defUnlockedFeature - -instance IsFeatureConfig MeetingConfig where - type FeatureSymbol MeetingConfig = "meeting" - featureSingleton = FeatureSingletonMeetingConfig - -instance ToObjectSchema MeetingConfig where - objectSchema = pure MeetingConfig - --------------------------------------------------------------------------------- --- MeetingPremium Feature --- --- Indicates whether a team has premium meeting features. When enabled, meetings --- created by team members are not marked as trial. When disabled, meetings are trial. - -data MeetingPremiumConfig = MeetingPremiumConfig - deriving (Eq, Show, Generic, GSOP.Generic) - deriving (Arbitrary) via (GenericUniform MeetingPremiumConfig) - deriving (RenderableSymbol) via (RenderableTypeName MeetingPremiumConfig) - deriving (ParseDbFeature, Default) via TrivialFeature MeetingPremiumConfig - -instance ToSchema MeetingPremiumConfig where - schema = object "MeetingPremiumConfig" objectSchema - -instance Default (LockableFeature MeetingPremiumConfig) where - def = defUnlockedFeature - -instance IsFeatureConfig MeetingPremiumConfig where - type FeatureSymbol MeetingPremiumConfig = "meetingPremium" - featureSingleton = FeatureSingletonMeetingPremiumConfig - -instance ToObjectSchema MeetingPremiumConfig where - objectSchema = pure MeetingPremiumConfig - --------------------------------------------------------------------------------- -- FeatureStatus @@ -2196,9 +2142,7 @@ type Features = SimplifiedUserConnectionRequestQRCodeConfig, AssetAuditLogConfig, StealthUsersConfig, - CellsInternalConfig, - MeetingConfig, - MeetingPremiumConfig + CellsInternalConfig ] -- | list of available features as a record diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 3170e3cee3a..ec5f20aeb33 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -228,14 +228,6 @@ settings: defaults: status: disabled lockStatus: locked - meeting: - defaults: - status: enabled - lockStatus: unlocked - meetingPremium: - defaults: - status: enabled - lockStatus: unlocked logLevel: Warn logNetStrings: false diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 356580e758d..5e738062b26 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -292,8 +292,6 @@ allFeaturesAPI = <@> featureAPI1Get <@> featureAPI1Full <@> featureAPI1Full - <@> featureAPI1Full - <@> featureAPI1Full featureAPI :: API IFeatureAPI GalleyEffects featureAPI = @@ -317,8 +315,6 @@ featureAPI = <@> mkNamedAPI @'("ilock", AppsConfig) (updateLockStatus @AppsConfig) <@> mkNamedAPI @'("ilock", SimplifiedUserConnectionRequestQRCodeConfig) (updateLockStatus @SimplifiedUserConnectionRequestQRCodeConfig) <@> mkNamedAPI @'("ilock", StealthUsersConfig) (updateLockStatus @StealthUsersConfig) - <@> mkNamedAPI @'("ilock", MeetingConfig) (updateLockStatus @MeetingConfig) - <@> mkNamedAPI @'("ilock", MeetingPremiumConfig) (updateLockStatus @MeetingPremiumConfig) -- all features <@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser) diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index dff22ef12b7..ccfd15e38d7 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -78,8 +78,6 @@ featureAPI = <@> mkNamedAPI @'("get", SimplifiedUserConnectionRequestQRCodeConfig) getFeature <@> mkNamedAPI @'("get", StealthUsersConfig) getFeature <@> mkNamedAPI @'("get", CellsInternalConfig) getFeature - <@> featureAPIGetPut @MeetingConfig - <@> featureAPIGetPut @MeetingPremiumConfig deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects deprecatedFeatureConfigAPI = diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 00befc792b1..72da3558e03 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -485,7 +485,3 @@ instance SetFeatureConfig AppsConfig instance SetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig instance SetFeatureConfig StealthUsersConfig - -instance SetFeatureConfig MeetingConfig - -instance SetFeatureConfig MeetingPremiumConfig diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index a990541e236..ff817e01696 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -428,10 +428,6 @@ instance GetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig instance GetFeatureConfig StealthUsersConfig -instance GetFeatureConfig MeetingConfig - -instance GetFeatureConfig MeetingPremiumConfig - -- | If second factor auth is enabled, make sure that end-points that don't support it, but -- should, are blocked completely. (This is a workaround until we have 2FA for those -- end-points as well.) diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 01fb0f364d8..a741892235c 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -192,10 +192,6 @@ sitemap' = :<|> Named @"put-route-apps-config" (mkFeatureStatusPutRoute @AppsConfig) :<|> Named @"get-route-stealth-users-config" (mkFeatureGetRoute @StealthUsersConfig) :<|> Named @"put-route-stealth-users-config" (mkFeatureStatusPutRoute @StealthUsersConfig) - :<|> Named @"get-route-meeting-config" (mkFeatureGetRoute @MeetingConfig) - :<|> Named @"put-route-meeting-config" (mkFeatureStatusPutRoute @MeetingConfig) - :<|> Named @"get-route-meeting-premium-config" (mkFeatureGetRoute @MeetingPremiumConfig) - :<|> Named @"put-route-meeting-premium-config" (mkFeatureStatusPutRoute @MeetingPremiumConfig) :<|> Named @"get-team-invoice" getTeamInvoice :<|> Named @"get-team-billing-info" getTeamBillingInfo :<|> Named @"put-team-billing-info" updateTeamBillingInfo @@ -230,8 +226,6 @@ sitemap' = :<|> Named @"lock-unlock-route-consumable-notifications-config" (mkFeatureLockUnlockRoute @ConsumableNotificationsConfig) :<|> Named @"lock-unlock-route-chat-bubbles-config" (mkFeatureLockUnlockRoute @ChatBubblesConfig) :<|> Named @"lock-unlock-route-apps-config" (mkFeatureLockUnlockRoute @AppsConfig) - :<|> Named @"lock-unlock-route-meeting-config" (mkFeatureLockUnlockRoute @MeetingConfig) - :<|> Named @"lock-unlock-route-meeting-premium-config" (mkFeatureLockUnlockRoute @MeetingPremiumConfig) sitemapInternal :: Servant.Server SternAPIInternal sitemapInternal = diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 905cd65ebd4..b08a9f9c644 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -327,10 +327,6 @@ type SternAPI = :<|> Named "put-route-apps-config" (MkFeatureStatusPutRoute AppsConfig) :<|> Named "get-route-stealth-users-config" (MkFeatureGetRoute StealthUsersConfig) :<|> Named "put-route-stealth-users-config" (MkFeatureStatusPutRoute StealthUsersConfig) - :<|> Named "get-route-meeting-config" (MkFeatureGetRoute MeetingConfig) - :<|> Named "put-route-meeting-config" (MkFeatureStatusPutRoute MeetingConfig) - :<|> Named "get-route-meeting-premium-config" (MkFeatureGetRoute MeetingPremiumConfig) - :<|> Named "put-route-meeting-premium-config" (MkFeatureStatusPutRoute MeetingPremiumConfig) :<|> Named "get-team-invoice" ( Summary "Get a specific invoice by Number" @@ -482,8 +478,6 @@ type SternAPI = :<|> Named "lock-unlock-route-consumable-notifications-config" (MkFeatureLockUnlockRoute ConsumableNotificationsConfig) :<|> Named "lock-unlock-route-chat-bubbles-config" (MkFeatureLockUnlockRoute ChatBubblesConfig) :<|> Named "lock-unlock-route-apps-config" (MkFeatureLockUnlockRoute AppsConfig) - :<|> Named "lock-unlock-route-meeting-config" (MkFeatureLockUnlockRoute MeetingConfig) - :<|> Named "lock-unlock-route-meeting-premium-config" (MkFeatureLockUnlockRoute MeetingPremiumConfig) ------------------------------------------------------------------------------- -- Swagger From d1ecba6c528fd4048b38e14a9f45fbbb3d43b705 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Fri, 19 Dec 2025 16:36:59 +0100 Subject: [PATCH 33/60] reject MLS messages while in epoch 0 (#4811) --- changelog.d/3-bug-fixes/mls-message-epoch-0 | 1 + integration/test/API/Galley.hs | 2 +- integration/test/MLS/Util.hs | 8 +++++-- integration/test/Test/MLS.hs | 22 +++++++++++++++++++ integration/test/Test/MLS/Reset.hs | 15 ++++++++----- services/galley/src/Galley/API/MLS/Message.hs | 7 ++++-- 6 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 changelog.d/3-bug-fixes/mls-message-epoch-0 diff --git a/changelog.d/3-bug-fixes/mls-message-epoch-0 b/changelog.d/3-bug-fixes/mls-message-epoch-0 new file mode 100644 index 00000000000..723e838f4b7 --- /dev/null +++ b/changelog.d/3-bug-fixes/mls-message-epoch-0 @@ -0,0 +1 @@ +Reject messages in MLS groups while in epoch 0. diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index c6295e4463a..96fb09923b8 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -902,7 +902,7 @@ getSelfMember user conv = do req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", domain, cnv, "self"]) submit "GET" req -resetConversation :: (MakesValue user) => user -> String -> Word64 -> App Response +resetConversation :: (HasCallStack, MakesValue user) => user -> String -> Word64 -> App Response resetConversation user groupId epoch = do req <- baseRequest user Galley Versioned (joinHttpPath ["mls", "reset-conversation"]) let payload = object ["group_id" .= groupId, "epoch" .= epoch] diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 8e50d2998c0..827b317b158 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -1015,8 +1015,8 @@ removeMemberFromChannel user channel userToBeRemoved = do } resetMLSConversation :: - (HasCallStack, MakesValue cid, MakesValue conv) => - cid -> + (HasCallStack, MakesValue conv) => + ClientIdentity -> conv -> App Value resetMLSConversation cid conv = do @@ -1043,4 +1043,8 @@ resetMLSConversation cid conv = do ) $ Map.delete convId mls.convs } + + mlsConv' <- getMLSConv convId' + keys <- getMLSPublicKeys cid >>= getJSON 200 + resetClientGroup mlsConv'.ciphersuite cid mlsConv'.groupId convId' keys pure conv' diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 3d76fd7ff39..98fe4dbfe5f 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -93,6 +93,28 @@ testPastStaleApplicationMessage otherDomain = do -- bob's application messages are now rejected void $ postMLSMessage bob1 msg2.message >>= getJSON 409 +testEpochZeroApplicationMessage :: (HasCallStack) => App () +testEpochZeroApplicationMessage = do + [alice] <- createAndConnectUsers [make OwnDomain] + alice1 <- createMLSClient def alice + conv <- createNewGroup def alice1 + void $ createAddCommit alice1 conv [] >>= sendAndConsumeCommitBundle + mlsConv <- getMLSConv conv + + -- send message, make sure that's succeeding + msg <- createApplicationMessage mlsConv.convId alice1 "group is initialised" + postMLSMessage alice1 msg.message >>= assertStatus 201 + + -- reset conversation, so it exists on server and client with epoch 0 + convId' <- objConvId =<< resetMLSConversation alice1 conv + + -- send message, make sure that's failing + msg' <- createApplicationMessage convId' alice1 "group not initialised" + postMLSMessage alice1 msg'.message >>= flip withResponse \resp -> do + j <- getJSON 400 resp + j %. "label" `shouldMatch` "mls-protocol-error" + j %. "message" `shouldMatch` "Application messages at epoch 0 are not supported" + testFutureStaleApplicationMessage :: (HasCallStack) => App () testFutureStaleApplicationMessage = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OwnDomain] diff --git a/integration/test/Test/MLS/Reset.hs b/integration/test/Test/MLS/Reset.hs index fac4b0a32bc..082343af3be 100644 --- a/integration/test/Test/MLS/Reset.hs +++ b/integration/test/Test/MLS/Reset.hs @@ -62,7 +62,7 @@ testResetSelfConversation = do void $ createAddCommit alice1 convId [alice] >>= sendAndConsumeCommitBundle mlsConv <- getMLSConv convId - conv' <- resetMLSConversation alice conv + conv' <- resetMLSConversation alice1 conv conv' %. "group_id" `shouldNotMatch` (mlsConv.groupId :: String) conv' %. "epoch" `shouldMatchInt` 0 convId' <- objConvId conv' @@ -79,11 +79,14 @@ testResetOne2OneConversation :: (HasCallStack) => App () testResetOne2OneConversation = do [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void . replicateM 2 $ uploadNewKeyPackage def bob1 + void . for [alice1, bob1] $ \cid -> replicateM 2 $ uploadNewKeyPackage def cid otherDomain <- asString OtherDomain conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 convOwnerDomain <- asString $ conv %. "conversation.qualified_id.domain" - let user = if convOwnerDomain == otherDomain then bob else alice + let (user, cid, other) = + if convOwnerDomain == otherDomain + then (bob, bob1, alice) + else (alice, alice1, bob) convId <- objConvId (conv %. "conversation") resetOne2OneGroup def alice1 conv @@ -91,13 +94,13 @@ testResetOne2OneConversation = do void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundle mlsConv <- getMLSConv convId - conv' <- resetMLSConversation user (conv %. "conversation") + conv' <- resetMLSConversation cid (conv %. "conversation") conv' %. "group_id" `shouldNotMatch` (mlsConv.groupId :: String) conv' %. "epoch" `shouldMatchInt` 0 convId' <- objConvId conv' - resetOne2OneGroupGeneric def alice1 conv' (conv %. "public_keys") + resetOne2OneGroupGeneric def cid conv' (conv %. "public_keys") - void $ createAddCommit alice1 convId' [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit cid convId' [other] >>= sendAndConsumeCommitBundle conv'' <- getConversation user convId >>= getJSON 200 conv'' %. "epoch" `shouldMatchInt` 1 diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 47103a0d90f..0f99e0a916b 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -534,10 +534,13 @@ postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do for_ convOrSub.ciphersuite $ \ciphersuite -> do checkConversationOutOfSync mempty lConvOrSub ciphersuite - -- reject application messages older than 2 epochs - -- FUTUREWORK: consider rejecting this message if the conversation epoch is 0 + -- reject application messages for epoch 0 let epochInt :: Epoch -> Integer epochInt = fromIntegral . epochNumber + when (epochInt msg.epoch == 0) . throw $ + mlsProtocolError "Application messages at epoch 0 are not supported" + + -- reject application messages older than 2 epochs case convOrSub.mlsMeta.cnvmlsActiveData of Nothing -> throw $ mlsProtocolError "Application messages at epoch 0 are not supported" Just activeData -> From 8abc17124d2292a746e71ceb878c1521aa564fd9 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 19 Dec 2025 22:40:35 +0100 Subject: [PATCH 34/60] WPB-21964: introduce Wire Meetings feature flags (#4915) --- changelog.d/2-features/WPB-21964 | 3 + charts/galley/templates/configmap.yaml | 8 +++ charts/galley/values.yaml | 8 +++ .../src/developer/reference/config-options.md | 30 ++++++++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 8 +++ integration/integration.cabal | 2 + integration/test/Test/FeatureFlags/Meeting.hs | 31 ++++++++++ .../test/Test/FeatureFlags/MeetingPremium.hs | 30 ++++++++++ integration/test/Test/FeatureFlags/Util.hs | 4 +- libs/galley-types/src/Galley/Types/Teams.hs | 14 +++++ .../src/Wire/API/Routes/Internal/Galley.hs | 2 + .../Wire/API/Routes/Public/Galley/Feature.hs | 2 + libs/wire-api/src/Wire/API/Team/Feature.hs | 58 ++++++++++++++++++- services/galley/galley.integration.yaml | 8 +++ services/galley/src/Galley/API/Internal.hs | 4 ++ .../galley/src/Galley/API/Public/Feature.hs | 2 + .../galley/src/Galley/API/Teams/Features.hs | 4 ++ .../src/Galley/API/Teams/Features/Get.hs | 4 ++ tools/stern/src/Stern/API.hs | 6 ++ tools/stern/src/Stern/API/Routes.hs | 6 ++ tools/stern/test/integration/API.hs | 6 +- 21 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 changelog.d/2-features/WPB-21964 create mode 100644 integration/test/Test/FeatureFlags/Meeting.hs create mode 100644 integration/test/Test/FeatureFlags/MeetingPremium.hs diff --git a/changelog.d/2-features/WPB-21964 b/changelog.d/2-features/WPB-21964 new file mode 100644 index 00000000000..3e01c82a61f --- /dev/null +++ b/changelog.d/2-features/WPB-21964 @@ -0,0 +1,3 @@ +Add `meetingsPremium` feature flag to distinguish premium teams from trial teams. Meetings created by premium team members are marked as non-trial. Public endpoints: GET/PUT /teams/:tid/features/meetingsPremium. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetingsPremium and lock status management. + +Add `meetings` feature flag to control access to the meetings API. When disabled, all meetings endpoints return 403 Forbidden. The feature is enabled and unlocked by default. Public endpoints: GET/PUT /teams/:tid/features/meetings. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetings and lock status management. diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 5c9072a5169..57dc603a61c 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -218,5 +218,13 @@ data: stealthUsers: {{- toYaml .settings.featureFlags.stealthUsers | nindent 10 }} {{- end }} + {{- if .settings.featureFlags.meetings }} + meetings: + {{- toYaml .settings.featureFlags.meetings | nindent 10 }} + {{- end }} + {{- if .settings.featureFlags.meetingsPremium }} + meetingsPremium: + {{- toYaml .settings.featureFlags.meetingsPremium | nindent 10 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index d0ed584bf67..578b559ae56 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -288,6 +288,14 @@ config: defaults: status: disabled lockStatus: locked + meetings: + defaults: + status: enabled + lockStatus: unlocked + meetingsPremium: + defaults: + status: disabled + lockStatus: locked aws: region: "eu-west-1" proxy: {} diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index e6c126e997d..6ce12e9bf15 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -234,6 +234,36 @@ The `conferenceCalling` section is optional in `featureFlags`. If it is omitted See also: conference falling for personal accounts (below). +### Meetings + +The `meetings` feature flag controls whether a user can initiate a meetings. It is enabled and unlocked by default. If you want a different configuration, use the following syntax: + +```yaml +meetings: + defaults: + status: disabled|enabled + lockStatus: locked|unlocked +``` + +The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/meetings/(un)?locked`). + +The feature status for individual teams can be changed via the public API (if the feature is unlocked). + +### Meetings Premium + +The `meetingsPremium` feature flag controls whether a team has premium meetings features. When enabled, meetings created by team members are not marked as trial. When disabled, meetings are trial and limited to 25 minutes. It is enabled and unlocked by default. If you want a different configuration, use the following syntax: + +```yaml +meetingsPremium: + defaults: + status: disabled|enabled + lockStatus: locked|unlocked +``` + +The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/meetingsPremium/(un)?locked`). + +The feature status for individual teams can be changed via the public API (if the feature is unlocked). + ### File Sharing File sharing is enabled and unlocked by default. If you want a different configuration, use the following syntax: diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index ad1501c8d86..eb8c96576a0 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -393,6 +393,14 @@ galley: defaults: status: disabled lockStatus: locked + meetings: + defaults: + status: enabled + lockStatus: unlocked + meetingsPremium: + defaults: + status: disabled + lockStatus: locked journal: endpoint: http://fake-aws-sqs:4568 queueName: integration-team-events.fifo diff --git a/integration/integration.cabal b/integration/integration.cabal index 03df02b4383..555ca6a7f1b 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -153,6 +153,8 @@ library Test.FeatureFlags.FileSharing Test.FeatureFlags.GuestLinks Test.FeatureFlags.LegalHold + Test.FeatureFlags.Meeting + Test.FeatureFlags.MeetingPremium Test.FeatureFlags.Mls Test.FeatureFlags.MlsE2EId Test.FeatureFlags.MlsMigration diff --git a/integration/test/Test/FeatureFlags/Meeting.hs b/integration/test/Test/FeatureFlags/Meeting.hs new file mode 100644 index 00000000000..4ed9018952a --- /dev/null +++ b/integration/test/Test/FeatureFlags/Meeting.hs @@ -0,0 +1,31 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 Test.FeatureFlags.Meeting where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchMeeting :: (HasCallStack) => App () +testPatchMeeting = checkPatch OwnDomain "meetings" enabled + +testMeeting :: (HasCallStack) => APIAccess -> App () +testMeeting access = + mkFeatureTests "meetings" + & addUpdate disabled + & addUpdate enabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/MeetingPremium.hs b/integration/test/Test/FeatureFlags/MeetingPremium.hs new file mode 100644 index 00000000000..ecbd2098684 --- /dev/null +++ b/integration/test/Test/FeatureFlags/MeetingPremium.hs @@ -0,0 +1,30 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 Test.FeatureFlags.MeetingPremium where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchMeetingPremium :: (HasCallStack) => App () +testPatchMeetingPremium = checkPatch OwnDomain "meetingsPremium" disabledLocked + +testMeetingPremium :: (HasCallStack) => APIAccess -> App () +testMeetingPremium access = + mkFeatureTests "meetingsPremium" + & addUpdate enabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 3c7f617d8ca..0f0cf793197 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -240,7 +240,9 @@ defAllFeatures = "collabora" .= object ["edition" .= "COOL"], "storage" .= object ["teamQuotaBytes" .= "1000000000000"] ] - ] + ], + "meetings" .= enabled, + "meetingsPremium" .= disabledLocked ] hasExplicitLockStatus :: String -> Bool diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index d63df1d3f79..6177db2ef4e 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -330,6 +330,20 @@ newtype instance FeatureDefaults StealthUsersConfig deriving (FromJSON) via Defaults (LockableFeature StealthUsersConfig) deriving (ParseFeatureDefaults) via OptionalField StealthUsersConfig +newtype instance FeatureDefaults MeetingsConfig + = MeetingDefaults (LockableFeature MeetingsConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MeetingsConfig) + deriving (ParseFeatureDefaults) via OptionalField MeetingsConfig + +newtype instance FeatureDefaults MeetingsPremiumConfig + = MeetingPremiumDefaults (LockableFeature MeetingsPremiumConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MeetingsPremiumConfig) + deriving (ParseFeatureDefaults) via OptionalField MeetingsPremiumConfig + featureKey :: forall cfg. (IsFeatureConfig cfg) => Key.Key featureKey = Key.fromText $ featureName @cfg diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index 6dedd4ccdd6..1293fcd7aa7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -94,6 +94,8 @@ type IFeatureAPI = :<|> IFeatureStatusLockStatusPut AppsConfig :<|> IFeatureStatusLockStatusPut SimplifiedUserConnectionRequestQRCodeConfig :<|> IFeatureStatusLockStatusPut StealthUsersConfig + :<|> IFeatureStatusLockStatusPut MeetingsConfig + :<|> IFeatureStatusLockStatusPut MeetingsPremiumConfig -- all feature configs :<|> Named "feature-configs-internal" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index d0c02b34126..b326f6e7715 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -78,6 +78,8 @@ type FeatureAPI = :<|> FeatureAPIGet SimplifiedUserConnectionRequestQRCodeConfig :<|> FeatureAPIGet StealthUsersConfig :<|> FeatureAPIGet CellsInternalConfig + :<|> FeatureAPIGetPut MeetingsConfig + :<|> FeatureAPIGetPut MeetingsPremiumConfig type VersionedFeatureAPIPut named reqBodyVersion cfg = Named diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 0e323507d49..5293d5c47b9 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -111,6 +111,8 @@ module Wire.API.Team.Feature AppsConfig (..), SimplifiedUserConnectionRequestQRCodeConfig (..), StealthUsersConfig (..), + MeetingsConfig (..), + MeetingsPremiumConfig (..), Features, AllFeatures, NpProject (..), @@ -275,6 +277,8 @@ data FeatureSingleton cfg where FeatureSingletonAssetAuditLogConfig :: FeatureSingleton AssetAuditLogConfig FeatureSingletonStealthUsersConfig :: FeatureSingleton StealthUsersConfig FeatureSingletonCellsInternalConfig :: FeatureSingleton CellsInternalConfig + FeatureSingletonMeetingsConfig :: FeatureSingleton MeetingsConfig + FeatureSingletonMeetingsPremiumConfig :: FeatureSingleton MeetingsPremiumConfig type family DeprecatedFeatureName (v :: Version) (cfg :: Type) :: Symbol @@ -2049,6 +2053,56 @@ instance IsFeatureConfig StealthUsersConfig where type FeatureSymbol StealthUsersConfig = "stealthUsers" featureSingleton = FeatureSingletonStealthUsersConfig +-------------------------------------------------------------------------------- +-- Meetings Feature +-- +-- Controls whether meetings functionality is available. When enabled, users can +-- create and manage meetings. When disabled, meetings endpoints are not accessible. + +data MeetingsConfig = MeetingsConfig + deriving (Eq, Show, Generic, GSOP.Generic) + deriving (Arbitrary) via (GenericUniform MeetingsConfig) + deriving (RenderableSymbol) via (RenderableTypeName MeetingsConfig) + deriving (ParseDbFeature, Default) via TrivialFeature MeetingsConfig + +instance ToSchema MeetingsConfig where + schema = object "MeetingsConfig" objectSchema + +instance Default (LockableFeature MeetingsConfig) where + def = defUnlockedFeature + +instance IsFeatureConfig MeetingsConfig where + type FeatureSymbol MeetingsConfig = "meetings" + featureSingleton = FeatureSingletonMeetingsConfig + +instance ToObjectSchema MeetingsConfig where + objectSchema = pure MeetingsConfig + +-------------------------------------------------------------------------------- +-- MeetingPremium Feature +-- +-- Indicates whether a team has premium meetings features. When enabled, meetings +-- created by team members are not marked as trial. When disabled, meetings are trial. + +data MeetingsPremiumConfig = MeetingsPremiumConfig + deriving (Eq, Show, Generic, GSOP.Generic) + deriving (Arbitrary) via (GenericUniform MeetingsPremiumConfig) + deriving (RenderableSymbol) via (RenderableTypeName MeetingsPremiumConfig) + deriving (ParseDbFeature, Default) via TrivialFeature MeetingsPremiumConfig + +instance ToSchema MeetingsPremiumConfig where + schema = object "MeetingsPremiumConfig" objectSchema + +instance Default (LockableFeature MeetingsPremiumConfig) where + def = defLockedFeature + +instance IsFeatureConfig MeetingsPremiumConfig where + type FeatureSymbol MeetingsPremiumConfig = "meetingsPremium" + featureSingleton = FeatureSingletonMeetingsPremiumConfig + +instance ToObjectSchema MeetingsPremiumConfig where + objectSchema = pure MeetingsPremiumConfig + --------------------------------------------------------------------------------- -- FeatureStatus @@ -2142,7 +2196,9 @@ type Features = SimplifiedUserConnectionRequestQRCodeConfig, AssetAuditLogConfig, StealthUsersConfig, - CellsInternalConfig + CellsInternalConfig, + MeetingsConfig, + MeetingsPremiumConfig ] -- | list of available features as a record diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index ec5f20aeb33..6d6369ff3d9 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -228,6 +228,14 @@ settings: defaults: status: disabled lockStatus: locked + meetings: + defaults: + status: enabled + lockStatus: unlocked + meetingsPremium: + defaults: + status: disabled + lockStatus: locked logLevel: Warn logNetStrings: false diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 5e738062b26..fd6aae3d52f 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -292,6 +292,8 @@ allFeaturesAPI = <@> featureAPI1Get <@> featureAPI1Full <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full featureAPI :: API IFeatureAPI GalleyEffects featureAPI = @@ -315,6 +317,8 @@ featureAPI = <@> mkNamedAPI @'("ilock", AppsConfig) (updateLockStatus @AppsConfig) <@> mkNamedAPI @'("ilock", SimplifiedUserConnectionRequestQRCodeConfig) (updateLockStatus @SimplifiedUserConnectionRequestQRCodeConfig) <@> mkNamedAPI @'("ilock", StealthUsersConfig) (updateLockStatus @StealthUsersConfig) + <@> mkNamedAPI @'("ilock", MeetingsConfig) (updateLockStatus @MeetingsConfig) + <@> mkNamedAPI @'("ilock", MeetingsPremiumConfig) (updateLockStatus @MeetingsPremiumConfig) -- all features <@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser) diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index ccfd15e38d7..09e315fc0ba 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -78,6 +78,8 @@ featureAPI = <@> mkNamedAPI @'("get", SimplifiedUserConnectionRequestQRCodeConfig) getFeature <@> mkNamedAPI @'("get", StealthUsersConfig) getFeature <@> mkNamedAPI @'("get", CellsInternalConfig) getFeature + <@> featureAPIGetPut @MeetingsConfig + <@> featureAPIGetPut @MeetingsPremiumConfig deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects deprecatedFeatureConfigAPI = diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 72da3558e03..3804928ee57 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -485,3 +485,7 @@ instance SetFeatureConfig AppsConfig instance SetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig instance SetFeatureConfig StealthUsersConfig + +instance SetFeatureConfig MeetingsConfig + +instance SetFeatureConfig MeetingsPremiumConfig diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index ff817e01696..e99b035ccc9 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -428,6 +428,10 @@ instance GetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig instance GetFeatureConfig StealthUsersConfig +instance GetFeatureConfig MeetingsConfig + +instance GetFeatureConfig MeetingsPremiumConfig + -- | If second factor auth is enabled, make sure that end-points that don't support it, but -- should, are blocked completely. (This is a workaround until we have 2FA for those -- end-points as well.) diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index a741892235c..db21771a9a6 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -192,6 +192,10 @@ sitemap' = :<|> Named @"put-route-apps-config" (mkFeatureStatusPutRoute @AppsConfig) :<|> Named @"get-route-stealth-users-config" (mkFeatureGetRoute @StealthUsersConfig) :<|> Named @"put-route-stealth-users-config" (mkFeatureStatusPutRoute @StealthUsersConfig) + :<|> Named @"get-route-meetings-config" (mkFeatureGetRoute @MeetingsConfig) + :<|> Named @"put-route-meetings-config" (mkFeatureStatusPutRoute @MeetingsConfig) + :<|> Named @"get-route-meetings-premium-config" (mkFeatureGetRoute @MeetingsPremiumConfig) + :<|> Named @"put-route-meetings-premium-config" (mkFeatureStatusPutRoute @MeetingsPremiumConfig) :<|> Named @"get-team-invoice" getTeamInvoice :<|> Named @"get-team-billing-info" getTeamBillingInfo :<|> Named @"put-team-billing-info" updateTeamBillingInfo @@ -226,6 +230,8 @@ sitemap' = :<|> Named @"lock-unlock-route-consumable-notifications-config" (mkFeatureLockUnlockRoute @ConsumableNotificationsConfig) :<|> Named @"lock-unlock-route-chat-bubbles-config" (mkFeatureLockUnlockRoute @ChatBubblesConfig) :<|> Named @"lock-unlock-route-apps-config" (mkFeatureLockUnlockRoute @AppsConfig) + :<|> Named @"lock-unlock-route-meetings-config" (mkFeatureLockUnlockRoute @MeetingsConfig) + :<|> Named @"lock-unlock-route-meetings-premium-config" (mkFeatureLockUnlockRoute @MeetingsPremiumConfig) sitemapInternal :: Servant.Server SternAPIInternal sitemapInternal = diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index b08a9f9c644..e50562ee9dc 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -327,6 +327,10 @@ type SternAPI = :<|> Named "put-route-apps-config" (MkFeatureStatusPutRoute AppsConfig) :<|> Named "get-route-stealth-users-config" (MkFeatureGetRoute StealthUsersConfig) :<|> Named "put-route-stealth-users-config" (MkFeatureStatusPutRoute StealthUsersConfig) + :<|> Named "get-route-meetings-config" (MkFeatureGetRoute MeetingsConfig) + :<|> Named "put-route-meetings-config" (MkFeatureStatusPutRoute MeetingsConfig) + :<|> Named "get-route-meetings-premium-config" (MkFeatureGetRoute MeetingsPremiumConfig) + :<|> Named "put-route-meetings-premium-config" (MkFeatureStatusPutRoute MeetingsPremiumConfig) :<|> Named "get-team-invoice" ( Summary "Get a specific invoice by Number" @@ -478,6 +482,8 @@ type SternAPI = :<|> Named "lock-unlock-route-consumable-notifications-config" (MkFeatureLockUnlockRoute ConsumableNotificationsConfig) :<|> Named "lock-unlock-route-chat-bubbles-config" (MkFeatureLockUnlockRoute ChatBubblesConfig) :<|> Named "lock-unlock-route-apps-config" (MkFeatureLockUnlockRoute AppsConfig) + :<|> Named "lock-unlock-route-meetings-config" (MkFeatureLockUnlockRoute MeetingsConfig) + :<|> Named "lock-unlock-route-meetings-premium-config" (MkFeatureLockUnlockRoute MeetingsPremiumConfig) ------------------------------------------------------------------------------- -- Swagger diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 903337fdc81..7d80f83d9f4 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -124,7 +124,11 @@ tests s = test s "PUT /teams/:tid/features/chatBubbles{,'?lockOrUnlock'}" $ testLockStatus @ChatBubblesConfig, test s "/teams/:tid/features/chatBubbles" $ testFeatureStatus @ChatBubblesConfig, test s "PUT /teams/:tid/features/apps{,'?lockOrUnlock'}" $ testLockStatus @AppsConfig, - test s "/teams/:tid/features/apps" $ testFeatureStatus @AppsConfig + test s "/teams/:tid/features/apps" $ testFeatureStatus @AppsConfig, + test s "PUT /teams/:tid/features/meetings{,'?lockOrUnlock'}" $ testLockStatus @MeetingsConfig, + test s "/teams/:tid/features/meetings" $ testFeatureStatus @MeetingsConfig, + test s "PUT /teams/:tid/features/meetingsPremium{,'?lockOrUnlock'}" $ testLockStatus @MeetingsPremiumConfig, + test s "/teams/:tid/features/meetingsPremium" $ testFeatureStatus @MeetingsPremiumConfig -- The following endpoints can not be tested here because they require ibis: -- - `GET /teams/:tid/billing` -- - `GET /teams/:tid/invoice/:inr` From f55a39cc154c8d88c01b04f358ecd674563cbd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20L=C3=A4ll?= Date: Sat, 20 Dec 2025 19:38:35 +0200 Subject: [PATCH 35/60] WPB-21768: Add SCIM get/filter groups response pagination (#4874) * Minor refactor - say directly what the pagination state is, don't go through a function - `toSortBy` isn't used anywhere and won't fit with changes to PaginationState either - inline `mkPaginationState` * Add `toPage` to hscim * Add test * Thread pagination's startIndex and count through APIs * Add pagination to SCIM get groups * Use `UserGroupPageRequest` instead of `GroupSearch` * Add failing unit test * Fix test * Add hscim pagination unit test * Fix `toPage`, remove comment * Resolve page size types and conversions .. by being lazy and just using Int32 in wire-server interfaces, as this is what PageSize internally is anyway. * fixup! Resolve page size types and conversions * Add changelog * Update libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add RFC link to changelog. * Fix unit test. - pass a function to the property, not a constant. without this change, there is only one test run instead of the default of 100. - add test case for one page exactly fitting the data. - "Page sizes are unique" does not test anything that's not already covered by "sort order is accounted for". * Nit-pick: minimize diff size. * Add HasCallStack to hasMembers test helper for better error messages Adding HasCallStack constraint to the hasMembers function in the GroupSpec tests will provide better error messages when tests fail, making it easier to identify which specific assertion failed. * Change index and count query param types from Natural PosInt32. (Custom newtype foor Int32 with smart constructor.) * TODO. * Use more suitable Seq instead of List in `toPage`. * Fixup 1f4e3761f42a4f94b316d9a4cba1e873b92b38ca * Cherry-pick copilot's test corner cases. * make sanitize-pr * Move `toPage` to mock interpreter (only allowed use case). * make sanitize-pr (reaching a fix-point there somehow seems harder than it should be...) * Turn off false(?) warnings. * Fixup: warnings weren't false... :m|: * ormolu... * Play with integer types some more... * Don't return final empty page from `getAllPages` * Remove unneeded `fromIntegral`s .. as the arguments are already of the correct type. * Improve Copilot-generated tests - actually check startIndex defaulting when out of range - remove separate test where startIndex and count are provided as this is already tested above - add a second test with negative startIndex - fix comments * Use `Word` in pagination; resolve all comments * make sanitize-pr * indulge copilot. * Move pagination types from subsystem effects to wire-api. * Fix PageSize type. * Fix unit test. * Fix and rename PageSize smart constructor. * Fix start index offset in paginated scim response. --------- Co-authored-by: Matthias Fischmann Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- cassandra-schema.cql | 14 +- .../1-api-changes/add-scim-group-pagination | 1 + integration/test/API/Spar.hs | 11 +- integration/test/Test/Spar.hs | 66 ++++++++ libs/hscim/default.nix | 8 + libs/hscim/hscim.cabal | 4 + libs/hscim/src/Web/Scim/Class/Group.hs | 8 +- libs/hscim/src/Web/Scim/Server/Mock.hs | 44 ++++- libs/hscim/test/Test/Class/GroupSpec.hs | 63 ++++++- libs/wire-api/src/Wire/API/Pagination.hs | 23 ++- .../src/Wire/API/Routes/Internal/Brig.hs | 2 + .../src/Wire/API/UserGroup/Pagination.hs | 36 +++- .../wire-subsystems/src/Wire/BrigAPIAccess.hs | 2 +- .../src/Wire/BrigAPIAccess/Rpc.hs | 10 +- .../src/Wire/ConversationStore/Postgres.hs | 2 +- .../src/Wire/PaginationState.hs | 19 +-- libs/wire-subsystems/src/Wire/Postgres.hs | 5 + .../wire-subsystems/src/Wire/ScimSubsystem.hs | 3 +- .../src/Wire/ScimSubsystem/Interpreter.hs | 21 ++- .../src/Wire/UserGroupStore.hs | 22 +-- .../src/Wire/UserGroupStore/Postgres.hs | 52 +++--- .../src/Wire/UserGroupSubsystem.hs | 33 +--- .../Wire/UserGroupSubsystem/Interpreter.hs | 47 ++---- .../Wire/MockInterpreters/UserGroupStore.hs | 23 +-- .../UserGroupSubsystem/InterpreterSpec.hs | 157 +++++++++++------- services/brig/src/Brig/API/Internal.hs | 6 +- services/brig/src/Brig/API/Public.hs | 21 ++- services/spar/src/Spar/Scim/Group.hs | 5 +- 28 files changed, 461 insertions(+), 247 deletions(-) create mode 100644 changelog.d/1-api-changes/add-scim-group-pagination diff --git a/cassandra-schema.cql b/cassandra-schema.cql index b50d79c4e87..c069ebc9f98 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -1114,7 +1114,7 @@ CREATE TABLE galley_test.team_conv ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1295,7 +1295,7 @@ CREATE TABLE galley_test.member ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1381,7 +1381,7 @@ CREATE TABLE galley_test.member_remote_user ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1508,7 +1508,7 @@ CREATE TABLE galley_test.user ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1600,7 +1600,7 @@ CREATE TABLE galley_test.mls_group_member_client ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1654,7 +1654,7 @@ CREATE TABLE galley_test.conversation ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1698,7 +1698,7 @@ CREATE TABLE galley_test.subconversation ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 diff --git a/changelog.d/1-api-changes/add-scim-group-pagination b/changelog.d/1-api-changes/add-scim-group-pagination new file mode 100644 index 00000000000..a31aa7dc0f6 --- /dev/null +++ b/changelog.d/1-api-changes/add-scim-group-pagination @@ -0,0 +1 @@ +Add [pagination to SCIM groups](https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.4) in Spar /scim/v2/Groups \ No newline at end of file diff --git a/integration/test/API/Spar.hs b/integration/test/API/Spar.hs index 9fd18a9efdd..56a8338bf43 100644 --- a/integration/test/API/Spar.hs +++ b/integration/test/API/Spar.hs @@ -135,11 +135,18 @@ deleteScimUserGroup domain token groupId = do submit "DELETE" $ req & addHeader "Authorization" ("Bearer " <> token) filterScimUserGroup :: (HasCallStack, MakesValue domain) => domain -> String -> Maybe String -> App Response -filterScimUserGroup domain token mbFilter = do +filterScimUserGroup domain token mbFilter = filterScimUserGroupPaginate domain token mbFilter Nothing Nothing + +filterScimUserGroupPaginate :: (HasCallStack, MakesValue domain) => domain -> String -> Maybe String -> Maybe Int -> Maybe Int -> App Response +filterScimUserGroupPaginate domain token mbFilter mbStartIndex mbCount = do req <- baseRequest domain Spar Versioned "/scim/v2/Groups" submit "GET" $ req & scimCommonHeaders token - & maybe id (\f -> addQueryParams [("filter", f)]) mbFilter + & addQueryParams + ( maybe [] (\f -> [("filter", f)]) mbFilter + <> maybe [] (\startIndex -> [("startIndex", show startIndex)]) mbStartIndex + <> maybe [] (\count -> [("count", show count)]) mbCount + ) mkScimGroup :: String -> [Value] -> Value mkScimGroup name members = diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index b6df2edae78..3ea7e0271c3 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -449,6 +449,72 @@ testSparScimCreateGetSearchUserGroup = do (singleEmptyGroup %. "members" & asList) `shouldMatch` ([] :: [Value]) respGroup4.json `shouldMatch` singleEmptyGroup + -- 4. Pagination + let searchPage substr startIndex count = + filterScimUserGroupPaginate + OwnDomain + tok + (Just $ "displayName co \"" <> substr <> "\"") + (Just startIndex) + (Just count) + createGroup name = createScimUserGroup OwnDomain tok $ mkScimGroup name [mkScimUser scimUserId] + + -- Create 20 groups + let expectedTotalResults = 20 :: Int + forM_ [1 .. expectedTotalResults] $ \n -> createGroup ("newGroupNo" <> show n) + + -- Go through 4 pages (the last one is an empty page) + forM_ [1 .. 4] $ \p -> + let startIndex = (p - 1) * count + 1 -- 1-based index + count = 7 + expectedItemsPerPage = max 0 (min count (expectedTotalResults - startIndex + 1)) -- expected between 0 and `count` depending on if it's a full, half or empty page + in searchPage "newGroupNo" startIndex count `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` startIndex + resp.json %. "totalResults" `shouldMatchInt` expectedTotalResults + resp.json %. "itemsPerPage" `shouldMatchInt` expectedItemsPerPage + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` expectedItemsPerPage + + -- startIndex=0 edge case: the 0 is treated as 1 according to SCIM spec + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") (Just 0) (Just 5) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` 5 + + -- startIndex=-2 edge case: -2 is treated as 1 according to SCIM spec + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") (Just (-2)) (Just 9) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` 9 + + -- Only startIndex, no count + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") (Just 5) Nothing `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 5 + resp.json %. "totalResults" `shouldMatchInt` expectedTotalResults + + -- Only count, no startIndex + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") Nothing (Just 3) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resp.json %. "itemsPerPage" `shouldMatchInt` 3 + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` 3 + + -- Filter with empty result + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"nonexistent-filter-xyz\"") (Just 1) (Just 10) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resp.json %. "totalResults" `shouldMatchInt` 0 + resp.json %. "itemsPerPage" `shouldMatchInt` 0 + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` 0 + + -- All results in one page + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") (Just 1) (Just 100) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resp.json %. "totalResults" `shouldMatchInt` expectedTotalResults + resp.json %. "itemsPerPage" `shouldMatchInt` expectedTotalResults + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` expectedTotalResults + testSparScimUpdateUserGroup :: (HasCallStack) => App () testSparScimUpdateUserGroup = do (alice, tid, []) <- createTeam OwnDomain 1 diff --git a/libs/hscim/default.nix b/libs/hscim/default.nix index 477d12f2e53..d0a64d40d04 100644 --- a/libs/hscim/default.nix +++ b/libs/hscim/default.nix @@ -10,6 +10,7 @@ , base , bytestring , case-insensitive +, containers , email-validate , gitignoreSource , hashable @@ -21,8 +22,10 @@ , http-api-data , http-media , http-types +, HUnit , hw-hspec-hedgehog , indexed-traversable +, lens-aeson , lib , list-t , microlens @@ -43,6 +46,7 @@ , time , utf8-string , uuid +, vector , wai , wai-extra , wai-utilities @@ -62,6 +66,7 @@ mkDerivation { base bytestring case-insensitive + containers email-validate hashable hspec @@ -113,14 +118,17 @@ mkDerivation { hspec-expectations hspec-wai http-types + HUnit hw-hspec-hedgehog indexed-traversable + lens-aeson microlens network-uri servant servant-server stm-containers text + vector wai wai-extra ]; diff --git a/libs/hscim/hscim.cabal b/libs/hscim/hscim.cabal index e29f6db05f6..73814d4a55e 100644 --- a/libs/hscim/hscim.cabal +++ b/libs/hscim/hscim.cabal @@ -92,6 +92,7 @@ library , base , bytestring , case-insensitive + , containers , email-validate , hashable , hspec @@ -219,14 +220,17 @@ test-suite spec , hspec-expectations , hspec-wai , http-types + , HUnit , hw-hspec-hedgehog , indexed-traversable + , lens-aeson , microlens , network-uri , servant , servant-server , stm-containers , text + , vector , wai , wai-extra diff --git a/libs/hscim/src/Web/Scim/Class/Group.hs b/libs/hscim/src/Web/Scim/Class/Group.hs index 943484696a7..5ac96dbb122 100644 --- a/libs/hscim/src/Web/Scim/Class/Group.hs +++ b/libs/hscim/src/Web/Scim/Class/Group.hs @@ -85,6 +85,8 @@ data GroupSite tag route = GroupSite { gsGetGroups :: route :- QueryParam "filter" Filter + :> QueryParam "startIndex" Int + :> QueryParam "count" Int :> Get '[SCIM] (ListResponse (StoredGroup tag)), gsGetGroup :: route @@ -119,6 +121,8 @@ class (Monad m, GroupTypes tag, AuthDB tag m) => GroupDB tag m where getGroups :: AuthInfo tag -> Maybe Filter -> + Maybe Int -> + Maybe Int -> ScimHandler m (ListResponse (StoredGroup tag)) -- | Get a single group by ID. @@ -179,9 +183,9 @@ groupServer :: GroupSite tag (AsServerT (ScimHandler m)) groupServer authData = GroupSite - { gsGetGroups = \mbFilter -> do + { gsGetGroups = \mbFilter mbStartIndex mbCount -> do auth <- authCheck @tag authData - getGroups @tag auth mbFilter, + getGroups @tag auth mbFilter mbStartIndex mbCount, gsGetGroup = \gid -> do auth <- authCheck @tag authData getGroup @tag auth gid, diff --git a/libs/hscim/src/Web/Scim/Server/Mock.hs b/libs/hscim/src/Web/Scim/Server/Mock.hs index eb2dc9ce5ea..3b819c16621 100644 --- a/libs/hscim/src/Web/Scim/Server/Mock.hs +++ b/libs/hscim/src/Web/Scim/Server/Mock.hs @@ -1,4 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- This file is part of the Wire Server implementation. @@ -29,7 +30,11 @@ import Control.Monad.Reader import Control.Monad.STM (STM, atomically) import Data.Aeson import qualified Data.CaseInsensitive as CI +import qualified Data.Foldable as Fold import Data.Hashable +import Data.Maybe (fromMaybe) +import Data.Sequence (Seq) +import qualified Data.Sequence as Seq import Data.Text (Text, pack) import Data.Time.Calendar import Data.Time.Clock @@ -50,7 +55,7 @@ import Web.Scim.Schema.Error import Web.Scim.Schema.ListResponse import Web.Scim.Schema.Meta import Web.Scim.Schema.ResourceType -import Web.Scim.Schema.Schema (Schema (Group20, User20)) +import Web.Scim.Schema.Schema (Schema (Group20, ListResponse20, User20)) import Web.Scim.Schema.User hiding (displayName) -- | Tag used in the mock server. @@ -155,7 +160,7 @@ instance GroupTypes Mock where type GroupId Mock = Id instance GroupDB Mock TestServer where - getGroups () mbFilter = do + getGroups () mbFilter mbStartIndex mbCount = do m <- asks groupDB groups <- map snd <$> liftSTM (ListT.toList $ STMMap.listT m) case mbFilter of @@ -170,7 +175,7 @@ instance GroupDB Mock TestServer where in pureSorted $ filter p groups _ -> throwScim $ badRequest InvalidFilter $ Just "Only displayName filter supported" where - pureSorted groups = pure $ fromList $ sortWith (Common.id . thing) groups + pureSorted groups = pure $ toPage (fromMaybe 1 mbStartIndex) mbCount $ sortWith (Common.id . thing) groups getGroup () gid = do m <- asks groupDB @@ -202,6 +207,31 @@ instance GroupDB Mock TestServer where Nothing -> throwScim (notFound "Group" (pack (show gid))) Just _ -> liftSTM $ STMMap.delete gid m +toPage :: forall a. Int -> Maybe Int -> [a] -> ListResponse a +toPage (max 1 -> startIx) mbCount list = case mbCount of + Nothing -> + ListResponse + { Web.Scim.Schema.ListResponse.schemas = [ListResponse20], + totalResults = totalResults', + startIndex = startIx, + itemsPerPage = Seq.length list', + resources = Fold.toList list' + } + Just count -> + let (page, _rest) = Seq.splitAt (fromIntegral safeCount) list' + safeCount = max 0 (min count maxBound) + in ListResponse + { Web.Scim.Schema.ListResponse.schemas = [ListResponse20], + totalResults = totalResults', + startIndex = startIx, + itemsPerPage = Seq.length page, + resources = Fold.toList page + } + where + totalResults' = length list + list' :: Seq a + list' = Seq.drop (startIx - 1) (Seq.fromList list) + ---------------------------------------------------------------------------- -- AuthDB @@ -234,9 +264,11 @@ createMeta rType = lastModified = testDate, version = Weak "testVersion", location = - Common.URI $ -- FUTUREWORK: getting the actual schema, authority, and path here - -- is a bit of work, but it may be required one day. - URI "https:" (Just $ URI.URIAuth "" "example.com" "") "/Users/id" "" "" + Common.URI + (URI "https:" (Just $ URI.URIAuth "" "example.com" "") "/Users/id" "" "") + -- FUTUREWORK: getting the actual schema, authority, and + -- path here is a bit of work, but it may be required one + -- day. } -- Natural transformation from our transformer stack to the Servant stack diff --git a/libs/hscim/test/Test/Class/GroupSpec.hs b/libs/hscim/test/Test/Class/GroupSpec.hs index a48f92aa3cf..8a2de4e85c2 100644 --- a/libs/hscim/test/Test/Class/GroupSpec.hs +++ b/libs/hscim/test/Test/Class/GroupSpec.hs @@ -22,21 +22,43 @@ module Test.Class.GroupSpec ) where +import Control.Monad +import qualified Data.Aeson as A +import qualified Data.Aeson.Lens as A +import qualified Data.ByteString as BS import Data.ByteString.Lazy (ByteString) +import Data.String +import qualified Data.Vector as V +import Lens.Micro import Network.Wai (Application) +import Network.Wai.Test import Servant (Proxy (Proxy)) import Servant.API.Generic +import Test.HUnit import Test.Hspec hiding (shouldSatisfy) import Test.Hspec.Wai hiding (patch, post, put, shouldRespondWith) import Web.Scim.Server (GroupAPI, groupServer, mkapp) import Web.Scim.Server.Mock import Web.Scim.Test.Util +fail_ :: (HasCallStack) => String -> WaiSession () a +fail_ = liftIO . assertFailure + +getJson :: BS.ByteString -> WaiSession () A.Value +getJson path' = + get path' + >>= ( \case + Just v -> pure v + Nothing -> fail_ "Response doesn't parse as JSON!" + ) + . A.decode + . simpleBody + app :: IO Application app = do storage <- emptyTestStorage let auth = Just "authorized" - pure $ + pure $ do mkapp @Mock (Proxy @(GroupAPI Mock)) (toServant (groupServer @Mock auth)) @@ -50,6 +72,45 @@ spec = with app $ do it "can insert then retrieve stored group" $ do post "/" adminGroup `shouldRespondWith` 201 get "/" `shouldRespondWith` groups + + it "paginates groups" $ do + forM_ [0 .. 4 :: Int] $ \_ -> post "/" adminGroup `shouldRespondWith` 201 + + let numFieldMatch :: A.Value -> String -> Int -> WaiSession () () + numFieldMatch v field expected = do + case v ^? A.key (fromString field) . A._Number of + Just gotten -> + when (gotten /= fromIntegral expected) $ do + fail_ $ field <> " is " <> show gotten <> " instead of " <> show expected + Nothing -> fail_ $ "missing field: " <> field + + hasMembers :: (HasCallStack) => WaiSession () A.Value -> Int -> Int -> Int -> WaiSession () () + hasMembers getPage expectedStartIndex expectedItemsPerPage expectedTotalResults = do + page :: A.Value <- getPage + case page ^? A.key "Resources" . A._Array of + Just v -> do + let l = V.length v + when (l /= expectedItemsPerPage) $ fail_ $ "ListResponse Resources has length " <> show l <> " instead of " <> show expectedItemsPerPage + Nothing -> fail_ "missing Resources field" + numFieldMatch page "totalResults" expectedTotalResults + numFieldMatch page "itemsPerPage" expectedItemsPerPage + numFieldMatch page "startIndex" expectedStartIndex + + hasMembers (getJson "/?startIndex=1&count=2") 1 2 5 + hasMembers (getJson "/?startIndex=3&count=2") 3 2 5 + hasMembers (getJson "/?startIndex=5&count=2") 5 1 5 + + hasMembers (getJson "/?startIndex=1&count=100") 1 5 5 + hasMembers (getJson "/?startIndex=6&count=100") 6 0 5 + + hasMembers (getJson "/?startIndex=1") 1 5 5 + hasMembers (getJson "/?startIndex=2") 2 4 5 + hasMembers (getJson "/?startIndex=3") 3 3 5 + hasMembers (getJson "/?startIndex=4") 4 2 5 + hasMembers (getJson "/?startIndex=5") 5 1 5 + hasMembers (getJson "/?startIndex=6") 6 0 5 + hasMembers (getJson "/") 1 5 5 + describe "GET /Groups/:id" $ do it "responds with 404 for unknown group" $ do get "/9999" `shouldRespondWith` 404 diff --git a/libs/wire-api/src/Wire/API/Pagination.hs b/libs/wire-api/src/Wire/API/Pagination.hs index beb4dda1e88..8fae686b1fa 100644 --- a/libs/wire-api/src/Wire/API/Pagination.hs +++ b/libs/wire-api/src/Wire/API/Pagination.hs @@ -22,10 +22,12 @@ import Data.Aeson qualified as A import Data.Bifunctor (first) import Data.Default import Data.OpenApi qualified as S +import Data.Proxy import Data.Range as Range import Data.Schema import Data.Text qualified as T import GHC.Generics +import GHC.TypeNats import Imports import Servant.API import Test.QuickCheck.Gen as Arbitrary @@ -69,25 +71,30 @@ instance S.ToParamSchema SortOrder where -------------------------------------------------------------------------------- -newtype PageSize = PageSize {fromPageSize :: Range 1 500 Int32} +newtype PageSize = PageSize {fromPageSize :: Range 0 MaxPageSize Word} deriving (Eq, Show, Ord, Generic) deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema PageSize pageSizeToInt :: PageSize -> Int -pageSizeToInt = fromIntegral . pageSizeToInt32 +pageSizeToInt = fromIntegral . pageSizeToWord -pageSizeToInt32 :: PageSize -> Int32 -pageSizeToInt32 = fromRange . fromPageSize +pageSizeToWord :: PageSize -> Word +pageSizeToWord = fromRange . fromPageSize -pageSizeFromInt :: Int32 -> Either Text PageSize +pageSizeFromInt :: Word -> Either Text PageSize pageSizeFromInt = fmap PageSize . first T.pack . Range.checkedEither +type MaxPageSize = 500 :: Nat + +maxPageSize :: (Num i) => i +maxPageSize = fromIntegral $ natVal (Proxy @MaxPageSize) + -- | Doesn't crash on bad input, but shrinks it into the allowed range. -pageSizeFromIntUnsafe :: Int32 -> PageSize -pageSizeFromIntUnsafe = PageSize . unsafeRange . (+ 1) . (`mod` 500) . (+ (-1)) +pageSizeFromIntegralTotal :: (Integral i) => i -> PageSize +pageSizeFromIntegralTotal = PageSize . unsafeRange . fromIntegral . min maxPageSize . max 0 instance Arbitrary PageSize where - arbitrary = pageSizeFromIntUnsafe <$> arbitrary + arbitrary = pageSizeFromIntegralTotal <$> (arbitrary @Int) instance ToSchema PageSize where schema = PageSize <$> fromPageSize .= schema diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index 3e69b5c8778..c14a2a2eb3b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -284,6 +284,8 @@ type GetGroupsInternal = :> Capture "tid" TeamId :> QueryParam' [Optional, Strict] "nameContains" Text.Text :> QueryParam' [Optional, Strict] "managedBy" ManagedBy + :> QueryParam' [Required, Strict] "startIndex" Word + :> QueryParam' [Optional, Strict] "count" Word :> Get '[Servant.JSON] UserGroupPageWithMembers ) diff --git a/libs/wire-api/src/Wire/API/UserGroup/Pagination.hs b/libs/wire-api/src/Wire/API/UserGroup/Pagination.hs index 92e309515e3..9e8ed1bb333 100644 --- a/libs/wire-api/src/Wire/API/UserGroup/Pagination.hs +++ b/libs/wire-api/src/Wire/API/UserGroup/Pagination.hs @@ -18,14 +18,48 @@ module Wire.API.UserGroup.Pagination where import Data.Aeson qualified as A +import Data.Default import Data.OpenApi qualified as S import Data.Schema +import Data.Time.Clock import GHC.Generics import Imports import Wire.API.Pagination +import Wire.API.User.Profile import Wire.API.UserGroup import Wire.Arbitrary as Arbitrary +-- | Request for a paginated list of user groups. +-- +-- (This is not technically API, but since it is used by several +-- different wire-subsystems we've moved it here anyway.) +data UserGroupPageRequest = UserGroupPageRequest + { searchString :: Maybe Text, + managedByFilter :: Maybe ManagedBy, + paginationState :: PaginationState UserGroupId, + sortOrder :: SortOrder, + pageSize :: PageSize, + includeMemberCount :: Bool, + includeChannels :: Bool + } + +instance Default UserGroupPageRequest where + def = + UserGroupPageRequest + { searchString = Nothing, + managedByFilter = Nothing, + paginationState = PaginationSortByCreatedAt Nothing, -- default sort by is 'createdAt', with no state + sortOrder = Desc, + pageSize = def, -- default is 15 + includeMemberCount = True, + includeChannels = False + } + +data PaginationState a + = PaginationSortByName (Maybe (Text, a)) + | PaginationSortByCreatedAt (Maybe (UTCTime, a)) + | PaginationOffset Word + -- | User group without members type UserGroupPage = UserGroupPage_ UserGroupMeta @@ -34,8 +68,6 @@ type UserGroupPageWithMembers = UserGroupPage_ UserGroup -- * User group pages --- - -- | User group pages with different types of user groups. data UserGroupPage_ a = UserGroupPage { page :: [a], diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs index 4feb73b6a23..34b90fe5b00 100644 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs @@ -161,7 +161,7 @@ data BrigAPIAccess m a where GetAccountsBy :: GetBy -> BrigAPIAccess m [User] CreateGroupInternal :: ManagedBy -> TeamId -> Maybe UserId -> NewUserGroup -> BrigAPIAccess m (Either Wai.Error UserGroup) GetGroupInternal :: TeamId -> UserGroupId -> Bool -> BrigAPIAccess m (Maybe UserGroup) - GetGroupsInternal :: TeamId -> Maybe Scim.Filter -> Maybe ManagedBy -> BrigAPIAccess m UserGroupPageWithMembers + GetGroupsInternal :: TeamId -> Maybe Scim.Filter -> Maybe ManagedBy -> Word -> Maybe Word -> BrigAPIAccess m UserGroupPageWithMembers UpdateGroup :: UpdateGroupInternalRequest -> BrigAPIAccess m (Either Wai.Error ()) DeleteGroupInternal :: ManagedBy -> TeamId -> UserGroupId -> BrigAPIAccess m (Either DeleteGroupManagedError ()) diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs index 541e3badf6e..bca65344eef 100644 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs @@ -124,8 +124,8 @@ interpretBrigAccess brigEndpoint = getAccountsBy localGetBy CreateGroupInternal managedBy teamId creatorUserId newGroup -> createGroupInternal managedBy teamId creatorUserId newGroup - GetGroupsInternal tid mbFilter mbManagedBy -> - getGroupsInternal tid mbFilter mbManagedBy + GetGroupsInternal tid mbFilter mbManagedBy startIndex mbCount -> + getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount GetGroupInternal tid gid includeChannels -> getGroupInternal tid gid includeChannels UpdateGroup req -> @@ -606,8 +606,10 @@ getGroupsInternal :: TeamId -> Maybe Scim.Filter -> Maybe ManagedBy -> + Word -> + Maybe Word -> Sem r UserGroupPageWithMembers -getGroupsInternal tid mbFilter mbManagedBy = do +getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount = do maybeDisplayName :: Maybe Text <- case mbFilter of Just filter' -> case filter' of FilterAttrCompare (AttrPath _schema "displayName" Nothing) OpCo (ValString str) -> pure $ Just str @@ -619,6 +621,8 @@ getGroupsInternal tid mbFilter mbManagedBy = do . paths ["i", "user-groups", toByteString' tid] . maybe id (queryItem "nameContains" . Text.encodeUtf8) maybeDisplayName . maybe id (queryItem "managedBy" . toByteString') mbManagedBy + . queryItem "startIndex" (toByteString' startIndex) + . maybe id (queryItem "count" . toByteString') mbCount . expect2xx decodeBodyOrThrow "brig" r diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs index 745c5255e1e..7d8b26ce82e 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs @@ -1284,7 +1284,7 @@ searchConversationsImpl req = ) -- keep ordering consistent with the outer query, therefore case-insensitive <> orderBy [("lower(name)", req.sortOrder), ("id", req.sortOrder)] - <> limit (pageSizeToInt32 req.pageSize) + <> limit (fromIntegral @_ @Int32 $ pageSizeToWord req.pageSize) <> literal ")" discoverableClause diff --git a/libs/wire-subsystems/src/Wire/PaginationState.hs b/libs/wire-subsystems/src/Wire/PaginationState.hs index 97baeb76c65..d5bf5562c08 100644 --- a/libs/wire-subsystems/src/Wire/PaginationState.hs +++ b/libs/wire-subsystems/src/Wire/PaginationState.hs @@ -18,33 +18,18 @@ module Wire.PaginationState ( PaginationState (..), paginationClause, - mkPaginationState, ) where -import Data.Time.Clock import Imports hiding (sortBy) -import Wire.API.Pagination +import Wire.API.UserGroup.Pagination import Wire.Postgres -data PaginationState a - = PaginationSortByName (Maybe (Text, a)) - | PaginationSortByCreatedAt (Maybe (UTCTime, a)) - paginationClause :: (PostgresValue a) => PaginationState a -> Maybe Clause paginationClause s = case s of PaginationSortByName (Just (name, x)) -> Just (mkClause "name" name <> mkClause "id" x) PaginationSortByCreatedAt (Just (createdAt, x)) -> Just (mkClause "created_at" createdAt <> mkClause "id" x) + PaginationOffset _ -> Nothing _ -> Nothing - -mkPaginationState :: - SortBy -> - Maybe Text -> - Maybe UTCTime -> - Maybe a -> - PaginationState a -mkPaginationState sortBy name createdAt x = case sortBy of - SortByName -> PaginationSortByName $ (,) <$> name <*> x - SortByCreatedAt -> PaginationSortByCreatedAt $ (,) <$> createdAt <*> x diff --git a/libs/wire-subsystems/src/Wire/Postgres.hs b/libs/wire-subsystems/src/Wire/Postgres.hs index 746bd701624..84cca14b183 100644 --- a/libs/wire-subsystems/src/Wire/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/Postgres.hs @@ -57,6 +57,7 @@ module Wire.Postgres clause1, orderBy, limit, + offset, buildStatement, -- * Type classes @@ -281,6 +282,10 @@ limit :: forall a. (PostgresValue a) => a -> QueryFragment limit n = paramLiteral (valueEncoder n) $ \i -> "limit " <> argPattern (postgresType @a) i +offset :: forall a. (PostgresValue a) => a -> QueryFragment +offset n = paramLiteral (valueEncoder n) $ \i -> + "offset " <> argPattern (postgresType @a) i + buildStatement :: QueryFragment -> Dec.Result b -> Statement () b buildStatement frag dec = Statement diff --git a/libs/wire-subsystems/src/Wire/ScimSubsystem.hs b/libs/wire-subsystems/src/Wire/ScimSubsystem.hs index 3f6d95da351..50ce62f3a6c 100644 --- a/libs/wire-subsystems/src/Wire/ScimSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/ScimSubsystem.hs @@ -20,6 +20,7 @@ module Wire.ScimSubsystem where import Data.Id +import Data.Int import Data.Maybe import Polysemy import Web.Scim.Class.Group qualified as SCG @@ -32,6 +33,6 @@ data ScimSubsystem m a where ScimGetUserGroup :: TeamId -> UserGroupId -> ScimSubsystem m (SCG.StoredGroup SparTag) ScimUpdateUserGroup :: TeamId -> UserGroupId -> SCG.Group -> ScimSubsystem m (SCG.StoredGroup SparTag) ScimDeleteUserGroup :: TeamId -> SCG.GroupId SparTag -> ScimSubsystem m () - ScimGetUserGroups :: TeamId -> Maybe Scim.Filter -> ScimSubsystem m (Scim.ListResponse (SCG.StoredGroup SparTag)) + ScimGetUserGroups :: TeamId -> Maybe Scim.Filter -> Maybe Int -> Maybe Int -> ScimSubsystem m (Scim.ListResponse (SCG.StoredGroup SparTag)) makeSem ''ScimSubsystem diff --git a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs index b946e189ed0..6ee81870342 100644 --- a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs @@ -40,6 +40,7 @@ import Web.Scim.Schema.Common qualified as Common import Web.Scim.Schema.ListResponse qualified as Scim import Web.Scim.Schema.Meta qualified as Meta import Web.Scim.Schema.ResourceType qualified as RT +import Web.Scim.Schema.Schema qualified as Scim import Wire.API.Routes.Internal.Brig import Wire.API.User import Wire.API.User.Scim (SparTag) @@ -63,7 +64,7 @@ interpretScimSubsystem :: interpretScimSubsystem = interpret $ \case ScimCreateUserGroup teamId scimGroup -> createScimGroupImpl teamId scimGroup ScimGetUserGroup tid gid -> scimGetUserGroupImpl tid gid - ScimGetUserGroups tid mbFilter -> scimGetUserGroupsImpl tid mbFilter + ScimGetUserGroups tid mbFilter startIndex mbCount -> scimGetUserGroupsImpl tid mbFilter startIndex mbCount ScimUpdateUserGroup teamId userGroupId scimGroup -> scimUpdateUserGroupImpl teamId userGroupId scimGroup ScimDeleteUserGroup teamId groupId -> deleteScimGroupImpl teamId groupId @@ -130,11 +131,23 @@ scimGetUserGroupsImpl :: ) => TeamId -> Maybe Scim.Filter -> + Maybe Int -> + Maybe Int -> Sem r (Scim.ListResponse (SCG.StoredGroup SparTag)) -scimGetUserGroupsImpl tid mbFilter = do - UserGroupPage {page} :: UserGroupPageWithMembers <- BrigAPI.getGroupsInternal tid mbFilter (Just ManagedByScim) +scimGetUserGroupsImpl tid mbFilter mbStartIndex mbCount = do + let startIndex = fromIntegral (max 0 $ maybe 0 (\n -> n - 1) mbStartIndex) :: Word + mbCount' = fromIntegral . (max 0) <$> mbCount + UserGroupPage {page, total} :: UserGroupPageWithMembers <- BrigAPI.getGroupsInternal tid mbFilter (Just ManagedByScim) startIndex mbCount' ScimSubsystemConfig scimBaseUri <- input - pure . Scim.fromList $ toStoredGroup scimBaseUri <$> page + let page' = map (toStoredGroup scimBaseUri) page + pure $ + Scim.ListResponse + { schemas = [Scim.ListResponse20], + totalResults = total, + itemsPerPage = length page', + startIndex = fromIntegral startIndex + 1, + resources = page' + } scimUpdateUserGroupImpl :: forall r. diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore.hs b/libs/wire-subsystems/src/Wire/UserGroupStore.hs index c8ccbbfae34..1dd366eda17 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore.hs @@ -25,36 +25,18 @@ import Data.Time.Clock import Data.Vector import Imports import Polysemy -import Wire.API.Pagination import Wire.API.User.Profile import Wire.API.UserGroup import Wire.API.UserGroup.Pagination -import Wire.PaginationState - -data UserGroupPageRequest = UserGroupPageRequest - { team :: TeamId, - searchString :: Maybe Text, - managedByFilter :: Maybe ManagedBy, - paginationState :: PaginationState UserGroupId, - sortOrder :: SortOrder, - pageSize :: PageSize, - includeMemberCount :: Bool, - includeChannels :: Bool - } userGroupCreatedAtPaginationState :: UserGroup_ f -> (UTCTime, UserGroupId) userGroupCreatedAtPaginationState ug = (fromUTCTimeMillis ug.createdAt, ug.id_) -toSortBy :: PaginationState UserGroupId -> SortBy -toSortBy = \case - PaginationSortByName _ -> SortByName - PaginationSortByCreatedAt _ -> SortByCreatedAt - data UserGroupStore m a where CreateUserGroup :: TeamId -> NewUserGroup -> ManagedBy -> UserGroupStore m UserGroup GetUserGroup :: TeamId -> UserGroupId -> Bool -> UserGroupStore m (Maybe UserGroup) - GetUserGroups :: UserGroupPageRequest -> UserGroupStore m UserGroupPage - GetUserGroupsWithMembers :: UserGroupPageRequest -> UserGroupStore m UserGroupPageWithMembers + GetUserGroups :: TeamId -> UserGroupPageRequest -> UserGroupStore m UserGroupPage + GetUserGroupsWithMembers :: TeamId -> UserGroupPageRequest -> UserGroupStore m UserGroupPageWithMembers GetUserGroupsForConv :: ConvId -> UserGroupStore m (Vector UserGroup) UpdateUserGroup :: TeamId -> UserGroupId -> UserGroupUpdate -> UserGroupStore m (Maybe ()) DeleteUserGroup :: TeamId -> UserGroupId -> UserGroupStore m (Maybe ()) diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs index ad6b218987b..997f3fb6ffc 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs @@ -48,7 +48,7 @@ import Wire.API.UserGroup hiding (UpdateUserGroupChannels) import Wire.API.UserGroup.Pagination import Wire.PaginationState import Wire.Postgres -import Wire.UserGroupStore (UserGroupPageRequest (..), UserGroupStore (..)) +import Wire.UserGroupStore (UserGroupStore (..)) type UserGroupStorePostgresEffectConstraints r = ( Member (Embed IO) r, @@ -64,8 +64,8 @@ interpretUserGroupStoreToPostgres = interpret $ \case CreateUserGroup team newUserGroup managedBy -> createUserGroup team newUserGroup managedBy GetUserGroup team userGroupId includeChannels -> getUserGroup team userGroupId includeChannels - GetUserGroups req -> getUserGroups req - GetUserGroupsWithMembers req -> getUserGroupsWithMembers req + GetUserGroups tid req -> getUserGroups tid req + GetUserGroupsWithMembers tid req -> getUserGroupsWithMembers tid req GetUserGroupsForConv convId -> getUserGroupsForConv convId UpdateUserGroup tid gid gup -> updateGroup tid gid gup DeleteUserGroup tid gid -> deleteGroup tid gid @@ -225,13 +225,14 @@ getUserGroupsWithMembers :: forall r. ( UserGroupStorePostgresEffectConstraints r ) => + TeamId -> UserGroupPageRequest -> Sem r UserGroupPageWithMembers -getUserGroupsWithMembers req = +getUserGroupsWithMembers tid req = runTransaction TxSessions.ReadCommitted TxSessions.Read $ UserGroupPage <$> Tx.statement () (refineResult (mapM toUserGroup) $ buildStatement query rows) - <*> getUserGroupCount req + <*> getUserGroupCount tid req where rows :: HD.Result [(UUID, Text, Int32, UTCTime, Vector UUID, Int32)] rows = @@ -262,7 +263,7 @@ getUserGroupsWithMembers req = "from user_group ug", "left join user_group_member gm on ug.id = gm.user_group_id" ] - <> [where_ (groupMatchIdName req <> groupPaginationWhereClause req)] + <> [where_ (groupMatchIdName tid req <> groupPaginationWhereClause req)] <> [ literal "group by ug.team_id, ug.id" ] <> groupPaginationOrderBy req @@ -277,9 +278,9 @@ getUserGroupsWithMembers req = members = Identity (fmap Id members' :: Vector UserId) pure $ UserGroup_ {..} -groupMatchIdName :: UserGroupPageRequest -> [QueryFragment] -groupMatchIdName req = - clause1 "ug.team_id" "=" req.team +groupMatchIdName :: TeamId -> UserGroupPageRequest -> [QueryFragment] +groupMatchIdName tid req = + clause1 "ug.team_id" "=" tid : managedByClause <> nameClause where @@ -297,22 +298,22 @@ groupPaginationWhereClause req = case paginationClause req.paginationState of groupPaginationOrderBy :: UserGroupPageRequest -> [QueryFragment] groupPaginationOrderBy req = - [ orderBy - [ (sortColumn req.paginationState, req.sortOrder), - ("ug.id", req.sortOrder) - ], - limit (pageSizeToInt32 req.pageSize) + [ orderBy $ + case req.paginationState of + PaginationSortByName _ -> [("ug.name", req.sortOrder), ("ug.id", req.sortOrder)] + PaginationSortByCreatedAt _ -> [("ug.created_at", req.sortOrder), ("ug.id", req.sortOrder)] + _ -> [("ug.id", req.sortOrder)], + limit $ + fromIntegral @_ @Int32 (pageSizeToWord req.pageSize) ] - where - sortColumn :: PaginationState a -> Text - sortColumn = \case - PaginationSortByName _ -> "ug.name" - PaginationSortByCreatedAt _ -> "ug.created_at" + <> case req.paginationState of + PaginationOffset n -> [offset (fromIntegral n :: Int32)] + _ -> [] -getUserGroupCount :: UserGroupPageRequest -> Tx.Transaction Int -getUserGroupCount req = Tx.statement () $ refineResult parseCount $ buildStatement query decoder +getUserGroupCount :: TeamId -> UserGroupPageRequest -> Tx.Transaction Int +getUserGroupCount tid req = Tx.statement () $ refineResult parseCount $ buildStatement query decoder where - query = literal "select count(*) from user_group ug" <> where_ (groupMatchIdName req) + query = literal "select count(*) from user_group ug" <> where_ (groupMatchIdName tid req) decoder = HD.singleRow (HD.column (HD.nonNullable HD.int8)) decodeUuidVector :: HD.Row (Vector UUID) @@ -335,12 +336,13 @@ getUserGroups :: ( UserGroupStorePostgresEffectConstraints r, Member (Input (Local ())) r ) => + TeamId -> UserGroupPageRequest -> Sem r UserGroupPage -getUserGroups req@(UserGroupPageRequest {..}) = do +getUserGroups tid req@(UserGroupPageRequest {..}) = do loc <- inputQualifyLocal () runTransaction TxSessions.ReadCommitted TxSessions.Read $ - UserGroupPage <$> getUserGroupsSession loc <*> getUserGroupCount req + UserGroupPage <$> getUserGroupsSession loc <*> getUserGroupCount tid req where getUserGroupsSession :: Local () -> Tx.Transaction [UserGroupMeta] getUserGroupsSession loc = @@ -351,7 +353,7 @@ getUserGroups req@(UserGroupPageRequest {..}) = do [ literal "select", literal selectors, literal "from user_group as ug", - where_ (groupMatchIdName req <> groupPaginationWhereClause req) + where_ (groupMatchIdName tid req <> groupPaginationWhereClause req) ] <> groupPaginationOrderBy req ) diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs index a674b7fb6a2..4f535f3537d 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs @@ -20,48 +20,19 @@ module Wire.UserGroupSubsystem where -import Data.Default import Data.Id -import Data.Time.Clock import Data.Vector (Vector) import Imports import Polysemy -import Wire.API.Pagination import Wire.API.Routes.Internal.Brig import Wire.API.User.Profile (ManagedBy) import Wire.API.UserGroup import Wire.API.UserGroup.Pagination -data GroupSearch = GroupSearch - { query :: Maybe Text, - sortBy :: Maybe SortBy, - sortOrder :: Maybe SortOrder, - pageSize :: Maybe PageSize, - lastName :: Maybe Text, - lastCreatedAt :: Maybe UTCTime, - lastId :: Maybe UserGroupId, - includeMemberCount :: Bool, - includeChannels :: Bool - } - -instance Default GroupSearch where - def = - GroupSearch - { query = Nothing, - sortBy = Nothing, - sortOrder = Nothing, - pageSize = Nothing, - lastName = Nothing, - lastCreatedAt = Nothing, - lastId = Nothing, - includeMemberCount = False, - includeChannels = False - } - data UserGroupSubsystem m a where CreateGroup :: UserId -> NewUserGroup -> UserGroupSubsystem m UserGroup GetGroup :: UserId -> UserGroupId -> Bool -> UserGroupSubsystem m (Maybe UserGroup) - GetGroups :: UserId -> GroupSearch -> UserGroupSubsystem m UserGroupPage + GetGroups :: UserId -> UserGroupPageRequest -> UserGroupSubsystem m UserGroupPage UpdateGroup :: UserId -> UserGroupId -> UserGroupUpdate -> UserGroupSubsystem m () DeleteGroup :: UserId -> UserGroupId -> UserGroupSubsystem m () DeleteGroupManaged :: ManagedBy -> TeamId -> UserGroupId -> UserGroupSubsystem m () @@ -75,7 +46,7 @@ data UserGroupSubsystem m a where -- Internal API handlers CreateGroupInternal :: ManagedBy -> TeamId -> Maybe UserId -> NewUserGroup -> UserGroupSubsystem r UserGroup GetGroupInternal :: TeamId -> UserGroupId -> Bool -> UserGroupSubsystem m (Maybe UserGroup) - GetGroupsInternal :: TeamId -> Maybe Text -> Maybe ManagedBy -> UserGroupSubsystem m UserGroupPageWithMembers + GetGroupsInternal :: TeamId -> Maybe Text -> Maybe ManagedBy -> Word -> Maybe Word -> UserGroupSubsystem m UserGroupPageWithMembers ResetUserGroupInternal :: UpdateGroupInternalRequest -> UserGroupSubsystem m () makeSem ''UserGroupSubsystem diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs index 527bb3fc084..1ed9de0bc2d 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs @@ -49,12 +49,10 @@ import Wire.BackgroundJobsPublisher import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess, internalGetConversation) import Wire.NotificationSubsystem -import Wire.PaginationState import Wire.Sem.Random qualified as Random import Wire.TeamSubsystem -import Wire.UserGroupStore (UserGroupPageRequest (..)) import Wire.UserGroupStore qualified as Store -import Wire.UserGroupSubsystem (GroupSearch (..), UserGroupSubsystem (..)) +import Wire.UserGroupSubsystem (UserGroupSubsystem (..)) import Wire.UserSubsystem (UserSubsystem, getLocalUserProfiles, getUserTeam) interpretUserGroupSubsystem :: @@ -86,7 +84,7 @@ interpretUserGroupSubsystem = interpret $ \case -- Internal API handlers CreateGroupInternal managedBy team mbCreator newGroup -> createUserGroupFullImpl managedBy team mbCreator newGroup GetGroupInternal tid gid includeChannels -> getUserGroupInternal tid gid includeChannels - GetGroupsInternal tid displayNameSubstring mbManagedBy -> getUserGroupsInternal tid displayNameSubstring mbManagedBy + GetGroupsInternal tid displayNameSubstring mbManagedBy startIndex mbCount -> getUserGroupsInternal tid displayNameSubstring mbManagedBy startIndex mbCount ResetUserGroupInternal req -> resetUserGroupInternal req data UserGroupSubsystemError @@ -250,29 +248,13 @@ getUserGroups :: Member (Error UserGroupSubsystemError) r ) => UserId -> - GroupSearch -> + UserGroupPageRequest -> Sem r UserGroupPage -getUserGroups getter search = do +getUserGroups getter pageReq = do team :: TeamId <- getUserTeam getter >>= ifNothing UserGroupNotATeamAdmin getterCanSeeAll :: Bool <- fromMaybe False <$> runMaybeT (mkGetterCanSeeAll getter team) unless getterCanSeeAll (throw UserGroupNotATeamAdmin) - let pageReq = - UserGroupPageRequest - { pageSize = fromMaybe def search.pageSize, - sortOrder = fromMaybe Desc search.sortOrder, - paginationState = - mkPaginationState - (fromMaybe def search.sortBy) - search.lastName - search.lastCreatedAt - search.lastId, - team = team, - searchString = search.query, - managedByFilter = Nothing, - includeMemberCount = search.includeMemberCount, - includeChannels = search.includeChannels - } - Store.getUserGroups pageReq + Store.getUserGroups team pageReq where ifNothing :: UserGroupSubsystemError -> Maybe a -> Sem r a ifNothing e = maybe (throw e) pure @@ -284,23 +266,21 @@ getUserGroupsInternal :: TeamId -> Maybe Text -> Maybe ManagedBy -> + Word -> + Maybe Word -> Sem r UserGroupPageWithMembers -getUserGroupsInternal team displayNameSubstring mbManagedBy = do - let -- hscim doesn't support pagination at the time of writing this, - -- so we better fit all groups into one page! - pageSize = pageSizeFromIntUnsafe 500 - pageReq = +getUserGroupsInternal team displayNameSubstring mbManagedBy startIndex mbCount = do + let pageReq = UserGroupPageRequest - { pageSize = pageSize, + { pageSize = maybe def pageSizeFromIntegralTotal mbCount, sortOrder = Asc, - paginationState = mkPaginationState SortByName (Just "displayName") Nothing Nothing, - team = team, + paginationState = PaginationOffset startIndex, searchString = displayNameSubstring, managedByFilter = mbManagedBy, includeMemberCount = True, includeChannels = False } - Store.getUserGroupsWithMembers pageReq + Store.getUserGroupsWithMembers team pageReq updateGroup :: ( Member UserSubsystem r, @@ -529,14 +509,13 @@ removeUserFromAllGroups uid tid = do go [] = pure () nextPage mug = - fmap (.page) . Store.getUserGroups $ + fmap (.page) . Store.getUserGroups tid $ UserGroupPageRequest { pageSize = def, sortOrder = Desc, paginationState = PaginationSortByCreatedAt $ fmap Store.userGroupCreatedAtPaginationState mug, - team = tid, searchString = Nothing, managedByFilter = Nothing, includeMemberCount = False, diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs index a7cbce4b3f6..20e55ebe8e9 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs @@ -28,7 +28,6 @@ import Wire.API.UserGroup hiding (UpdateUserGroupChannels) import Wire.API.UserGroup.Pagination import Wire.MockInterpreters.Now import Wire.MockInterpreters.Random -import Wire.PaginationState import Wire.Sem.Random qualified as Rnd import Wire.UserGroupStore @@ -62,8 +61,8 @@ userGroupStoreTestInterpreter = interpret $ \case CreateUserGroup tid ng mb -> createUserGroupImpl tid ng mb GetUserGroup tid gid includeChannels -> getUserGroupImpl tid gid includeChannels - GetUserGroups req -> getUserGroupsImpl req - GetUserGroupsWithMembers req -> getUserGroupsWithMembersImpl req + GetUserGroups tid req -> getUserGroupsImpl tid req + GetUserGroupsWithMembers tid req -> getUserGroupsWithMembersImpl tid req GetUserGroupsForConv cid -> getUserGroupsForConvImpl cid UpdateUserGroup tid gid gup -> updateUserGroupImpl tid gid gup DeleteUserGroup tid gid -> deleteUserGroupImpl tid gid @@ -124,16 +123,16 @@ filterChannels includeChannels ug = then (ug :: UserGroup) {channelsCount = Just $ maybe 0 length ug.channels} else (ug :: UserGroup) {channels = mempty} -getUserGroupsImpl :: (UserGroupStoreInMemEffectConstraints r) => UserGroupPageRequest -> Sem r UserGroupPage -getUserGroupsImpl req = do - UserGroupPage pages count <- getUserGroupsWithMembersImpl req +getUserGroupsImpl :: (UserGroupStoreInMemEffectConstraints r) => TeamId -> UserGroupPageRequest -> Sem r UserGroupPage +getUserGroupsImpl tid req = do + UserGroupPage pages count <- getUserGroupsWithMembersImpl tid req pure $ UserGroupPage (map removeMembers pages) count where removeMembers :: UserGroup -> UserGroupMeta removeMembers UserGroup_ {..} = UserGroup_ {members = Const (), ..} -getUserGroupsWithMembersImpl :: (UserGroupStoreInMemEffectConstraints r) => UserGroupPageRequest -> Sem r UserGroupPageWithMembers -getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do +getUserGroupsWithMembersImpl :: (UserGroupStoreInMemEffectConstraints r) => TeamId -> UserGroupPageRequest -> Sem r UserGroupPageWithMembers +getUserGroupsWithMembersImpl tid UserGroupPageRequest {..} = do meta <- ((snd <$>) . sieve . fmap (_2 %~ (filterChannels includeChannels)) . Map.toList) <$> get @UserGroupInMemState pure $ UserGroupPage meta (length meta) where @@ -154,7 +153,7 @@ getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do . narrowToManagedBy . narrowToTeam - narrowToTeam = filter (\((thisTid, _), _) -> thisTid == team) + narrowToTeam = filter (\((thisTid, _), _) -> thisTid == tid) narrowToManagedBy = filter (\(_, ug) -> maybe True (== ug.managedBy) managedByFilter) @@ -169,6 +168,8 @@ getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do (PaginationSortByName _, Desc) -> (n', i') `compare` (n, i) (PaginationSortByCreatedAt _, Asc) -> (c, i) `compare` (c', i') (PaginationSortByCreatedAt _, Desc) -> (c', i') `compare` (c, i) + (PaginationOffset _, Asc) -> i `compare` i' + (PaginationOffset _, Desc) -> i' `compare` i where n = ug.name n' = ug'.name @@ -178,7 +179,9 @@ getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do c' = ug'.createdAt dropBeforeStart = do - dropWhile sqlConds + case paginationState of + PaginationOffset n -> drop (fromIntegral n) + _ -> dropWhile sqlConds where sqlConds :: ((TeamId, UserGroupId), UserGroup) -> Bool sqlConds ((_, _), row) = diff --git a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs index d163e0b8122..019cd848e36 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs @@ -36,7 +36,6 @@ import Data.Set qualified as Set import Data.UUID qualified as UUID import Data.Vector qualified as V import Imports -import Numeric.Natural import Polysemy import Polysemy.Error import Polysemy.Input (Input, runInputConst) @@ -274,14 +273,14 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do getGroups (ownerId team) def - { query = Just (userGroupNameToText userGroupName) + { searchString = Just (userGroupNameToText userGroupName) } getGroupsOutsider <- try $ getGroups (ownerId otherTeam) def - { query = Just (userGroupNameToText userGroupName) + { searchString = Just (userGroupNameToText userGroupName) } pure $ getGroupAdmin === Just group1 @@ -308,13 +307,13 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do getGroups (ownerId team1) def - { query = Just (userGroupNameToText userGroupName1) + { searchString = Just (userGroupNameToText userGroupName1) } getOtherGroups <- getGroups (ownerId team1) def - { query = Just (userGroupNameToText userGroupName2) + { searchString = Just (userGroupNameToText userGroupName2) } pure $ @@ -329,10 +328,10 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do let newGroups = [newUserGroup (either undefined id $ userGroupNameFromText name) | name <- ["1", "2", "2", "33"]] groups <- (\ng -> passTime 1 >> createGroup (ownerId team1) ng) `mapM` newGroups - get0 <- getGroups (ownerId team1) def {query = Just "nope"} - get1 <- getGroups (ownerId team1) def {query = Just "1"} - get2 <- getGroups (ownerId team1) def {query = Just "2"} - get3 <- getGroups (ownerId team1) def {query = Just "3"} + get0 <- getGroups (ownerId team1) def {searchString = Just "nope"} + get1 <- getGroups (ownerId team1) def {searchString = Just "1"} + get2 <- getGroups (ownerId team1) def {searchString = Just "2"} + get3 <- getGroups (ownerId team1) def {searchString = Just "3"} pure do get0.page `shouldBe` [] @@ -342,54 +341,96 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do prop "getGroups: pagination (happy flow)" $ do \(WithMods team1 :: WithMods '[AtLeastOneNonAdmin] ArbitraryTeam) - numGroupsPre - pageSizePre -> - let numGroups = fromIntegral @Natural numGroupsPre + 1 - pageSize = - let smallify = (\case 0 -> 3; other -> other) . (`mod` (numGroups + 5)) - in PageSize . unsafeRange . smallify . fromRange . fromPageSize $ pageSizePre - in expectRight - . runDependencies (allUsers team1) (galleyTeam team1) - . interpretUserGroupSubsystem - $ do - let mkNewGroup = newUserGroup (either undefined id $ userGroupNameFromText "same name") - mkGroup = passTime 1 >> createGroup (ownerId team1) mkNewGroup - - -- groups are only distinguished by creation date - groups <- replicateM (fromIntegral numGroups) mkGroup - - results :: [UserGroupPage] <- do - let fetch mLastThing = do - p <- - getGroups - (ownerId team1) - def - { sortBy = Just SortByCreatedAt, - pageSize = Just pageSize, - lastName = fmap (userGroupNameToText . (.name)) mLastThing, - lastCreatedAt = fmap (fromUTCTimeMillis . (.createdAt)) mLastThing, - lastId = fmap (.id_) mLastThing - } + (Positive (Small (numGroups :: Int))) + (Positive (Small (pageSizeFromIntegralTotal @Int -> pageSize))) -> + expectRight + . runDependencies (allUsers team1) (galleyTeam team1) + . interpretUserGroupSubsystem + $ do + let mkNewGroup = newUserGroup (either undefined id $ userGroupNameFromText "same name") + mkGroup = passTime 1 >> createGroup (ownerId team1) mkNewGroup + + -- groups are only distinguished by creation date + groups <- replicateM numGroups mkGroup + + results :: [UserGroupPage] <- do + let fetch mLastThing = do + p <- + getGroups + (ownerId team1) + def + { paginationState = PaginationSortByCreatedAt $ (,) <$> fmap (fromUTCTimeMillis . (.createdAt)) mLastThing <*> fmap (.id_) mLastThing, + pageSize + } + if null p.page + then pure [] + else if length p.page < pageSizeToInt pageSize then pure [p] else (p :) <$> fetch (Just (last p.page)) - fetch Nothing - - let all' :: (x -> Property) -> [x] -> Property - all' mkProp = foldr (\x acc -> mkProp x .&&. acc) (True === True) - - assertLessThanOrEq :: (Show a, Ord a) => a -> a -> Property - assertLessThanOrEq x y = counterexample (show x <> "\n>\n" <> show y) $ x <= y - pure $ - -- result is complete and correct (`reverse` because `createdAt` defaults to `Desc`) - mconcat ((.page) <$> results) === (userGroupToMeta <$> reverse groups) - -- every page has the expected size - .&&. all' - (\r -> length r.page === pageSizeToInt pageSize) - (take (length results - 2) results) - .&&. all' - (\r -> length r.page `assertLessThanOrEq` pageSizeToInt pageSize) - (drop (length results - 2) results) + fetch Nothing + + let all' :: (x -> Property) -> [x] -> Property + all' mkProp = foldr (\x acc -> mkProp x .&&. acc) (True === True) + + assertLessThanOrEq :: (Show a, Ord a) => a -> a -> Property + assertLessThanOrEq x y = counterexample (show x <> "\n>\n" <> show y) $ x <= y + pure $ + Right pageSize === pageSizeFromInt 0 + .||. ( + -- result is complete and correct (`reverse` because `createdAt` defaults to `Desc`) + mconcat ((.page) <$> results) === (userGroupToMeta <$> reverse groups) + -- every page has the expected size + .&&. all' + (\r -> length r.page === pageSizeToInt pageSize) + (take (length results - 2) results) + .&&. all' + (\r -> length r.page `assertLessThanOrEq` pageSizeToInt pageSize) + (drop (length results - 2) results) + ) + + prop "getGroups: pagination via offset" $ \(WithMods team1 :: WithMods '[AtLeastOneNonAdmin] ArbitraryTeam) -> + runDependenciesFailOnError (allUsers team1) (galleyTeam team1) . interpretUserGroupSubsystem $ do + -- Create groups + groups <- forM ["1", "2", "3", "4", "5"] $ \name -> do + passTime 1 + createGroup (ownerId team1) $ newUserGroup $ UserGroupName $ unsafeRange name + let groupIds = map (.id_) groups :: [UserGroupId] + + -- Define helper to fetch all pages, providing desired + -- sortOrder and page size to the mock database + let getAllPages :: (Member UserGroupSubsystem r) => SortOrder -> Word -> Sem r [UserGroupPage] + getAllPages sortOrder' pageSize' = go 0 + where + go :: (Member UserGroupSubsystem r) => Word -> Sem r [UserGroupPage] + go offset = do + p <- + getGroups + (ownerId team1) + def + { paginationState = PaginationOffset offset, + pageSize = PageSize $ unsafeRange $ fromIntegral pageSize', + sortOrder = sortOrder' + } + let len = length p.page + if + | len > 0 && len < fromIntegral pageSize' -> pure [p] + | len == 0 -> pure [] + | otherwise -> (p :) <$> go (offset + pageSize') + + ascendingPages :: [UserGroupPage] <- getAllPages Asc 2 + descendingPages :: [UserGroupPage] <- getAllPages Desc 3 + exactlyOnePage :: [UserGroupPage] <- getAllPages Desc 5 + + pure do + -- Page sizes are as expected + map (length . (.page)) ascendingPages `shouldBe` [2, 2, 1] + map (length . (.page)) descendingPages `shouldBe` [3, 2] + map (length . (.page)) exactlyOnePage `shouldBe` [5] + + -- Sort order is accounted for, pages do not overlap + (map (.id_) . (.page) =<< ascendingPages) `shouldBe` sort groupIds + (map (.id_) . (.page) =<< descendingPages) `shouldBe` sortBy (comparing Down) groupIds it "getGroups (ordering)" $ do WithMods team1 :: WithMods '[AtLeastOneNonAdmin] ArbitraryTeam <- generate arbitrary @@ -412,15 +453,15 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do getGroups (ownerId team1) def - { sortBy = Just SortByName, - sortOrder = Just Desc + { paginationState = PaginationSortByName Nothing, + sortOrder = Desc } sortByCreatedAtAsc <- getGroups (ownerId team1) def - { sortBy = Just SortByCreatedAt, - sortOrder = Just Asc + { paginationState = PaginationSortByCreatedAt Nothing, + sortOrder = Asc } let expectSortByDefaults = [[group1b, group2b, group3b], [group1a, group2a, group3a]] diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index c7b555cf10e..69e76cc4e7e 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -1031,9 +1031,11 @@ getGroupsInternalH :: TeamId -> Maybe T.Text -> Maybe ManagedBy -> + Word -> + Maybe Word -> Handler r UserGroupPageWithMembers -getGroupsInternalH tid nameContains managedBy = - lift . liftSem $ getGroupsInternal tid nameContains managedBy +getGroupsInternalH tid nameContains managedBy startIndex mbCount = + lift . liftSem $ getGroupsInternal tid nameContains managedBy startIndex mbCount updateGroupInternalH :: ( Member UserGroupSubsystem r diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 05bf6b8f2ec..99202db89c8 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1707,18 +1707,17 @@ getUserGroups :: Bool -> Bool -> Handler r UserGroupPage -getUserGroups lusr q sortBy sortOrder pageSize lastName lastCreatedAt lastId includeChannels includeMemberCount = +getUserGroups lusr searchString sortBy sortOrder pageSize lastName lastCreatedAt lastId includeChannels includeMemberCount = lift . liftSem $ - UserGroup.getGroups - (tUnqualified lusr) - UserGroup.GroupSearch - { query = q, - sortBy, - sortOrder, - pageSize, - lastName = fmap userGroupNameToText lastName, - lastCreatedAt = fmap fromUTCTimeMillis lastCreatedAt, - lastId, + UserGroup.getGroups (tUnqualified lusr) $ + UserGroupPageRequest + { pageSize = fromMaybe def pageSize, + managedByFilter = Nothing, + sortOrder = fromMaybe Desc sortOrder, + paginationState = case fromMaybe def sortBy of + SortByName -> PaginationSortByName $ (,) <$> fmap userGroupNameToText lastName <*> lastId + SortByCreatedAt -> PaginationSortByCreatedAt $ (,) <$> fmap fromUTCTimeMillis lastCreatedAt <*> lastId, + searchString, includeMemberCount, includeChannels } diff --git a/services/spar/src/Spar/Scim/Group.hs b/services/spar/src/Spar/Scim/Group.hs index caa22bd5032..3ace67bcf7a 100644 --- a/services/spar/src/Spar/Scim/Group.hs +++ b/services/spar/src/Spar/Scim/Group.hs @@ -39,8 +39,11 @@ instance (AuthDB SparTag (Sem r), Member ScimSubsystem r) => SCG.GroupDB SparTag getGroups :: AuthInfo SparTag -> Maybe Filter -> + Maybe Int -> + Maybe Int -> ScimHandler (Sem r) (ListResponse (SCG.StoredGroup SparTag)) - getGroups ((.stiTeam) -> tid) mbFilter = lift $ scimGetUserGroups tid mbFilter + getGroups ((.stiTeam) -> tid) mbFilter mbStartIndex mbCount = + lift $ scimGetUserGroups tid mbFilter (fromIntegral <$> mbStartIndex) (fromIntegral <$> mbCount) -- \| Get a single group by ID. -- From 5a627db32794a5da533784260560aea69fc0933e Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Mon, 22 Dec 2025 11:38:46 +0100 Subject: [PATCH 36/60] simplify testResetOne2OneConversation (#4917) --- integration/test/Test/MLS/Reset.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/integration/test/Test/MLS/Reset.hs b/integration/test/Test/MLS/Reset.hs index 082343af3be..f899a202aca 100644 --- a/integration/test/Test/MLS/Reset.hs +++ b/integration/test/Test/MLS/Reset.hs @@ -83,10 +83,10 @@ testResetOne2OneConversation = do otherDomain <- asString OtherDomain conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 convOwnerDomain <- asString $ conv %. "conversation.qualified_id.domain" - let (user, cid, other) = + let (user, other) = if convOwnerDomain == otherDomain - then (bob, bob1, alice) - else (alice, alice1, bob) + then (bob1, alice) + else (alice1, bob) convId <- objConvId (conv %. "conversation") resetOne2OneGroup def alice1 conv @@ -94,13 +94,13 @@ testResetOne2OneConversation = do void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundle mlsConv <- getMLSConv convId - conv' <- resetMLSConversation cid (conv %. "conversation") + conv' <- resetMLSConversation user (conv %. "conversation") conv' %. "group_id" `shouldNotMatch` (mlsConv.groupId :: String) conv' %. "epoch" `shouldMatchInt` 0 convId' <- objConvId conv' - resetOne2OneGroupGeneric def cid conv' (conv %. "public_keys") + resetOne2OneGroupGeneric def user conv' (conv %. "public_keys") - void $ createAddCommit cid convId' [other] >>= sendAndConsumeCommitBundle + void $ createAddCommit user convId' [other] >>= sendAndConsumeCommitBundle conv'' <- getConversation user convId >>= getJSON 200 conv'' %. "epoch" `shouldMatchInt` 1 From 1bcafb96280333862f27dae1e1f0aaede92226f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20L=C3=A4ll?= Date: Tue, 23 Dec 2025 14:44:49 +0200 Subject: [PATCH 37/60] Find apps from `GET /search/contacts` (#4920) * Add test * Sync app user to elastic search * Add changelog --- changelog.d/3-bug-fixes/make-apps-findable | 1 + integration/test/Test/Apps.hs | 9 +++++++++ .../wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs | 8 ++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 changelog.d/3-bug-fixes/make-apps-findable diff --git a/changelog.d/3-bug-fixes/make-apps-findable b/changelog.d/3-bug-fixes/make-apps-findable new file mode 100644 index 00000000000..38173b19c94 --- /dev/null +++ b/changelog.d/3-bug-fixes/make-apps-findable @@ -0,0 +1 @@ +Make underlying users for apps findable from `GET /search/contacts` \ No newline at end of file diff --git a/integration/test/Test/Apps.hs b/integration/test/Test/Apps.hs index 4c07bd24b82..4af7b903b36 100644 --- a/integration/test/Test/Apps.hs +++ b/integration/test/Test/Apps.hs @@ -20,6 +20,7 @@ module Test.Apps where import API.Brig +import qualified API.BrigInternal as BrigI import SetupHelpers import Testlib.Prelude @@ -87,6 +88,14 @@ testCreateApp = do void $ bindResponse (createApp owner tid new {category = "notinenum"}) $ \resp -> do resp.status `shouldMatchInt` 400 + -- App's user is findable from /search/contacts + BrigI.refreshIndex OwnDomain + searchContacts owner new.name OwnDomain `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + docs <- resp.json %. "documents" >>= asList + foundUids <- for docs objId + foundUids `shouldMatch` [appId] + testRefreshAppCookie :: (HasCallStack) => App () testRefreshAppCookie = do (alice, tid, [bob]) <- createTeam OwnDomain 2 diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs index 1a901eb4204..b3f22d1e1e2 100644 --- a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs @@ -49,6 +49,7 @@ import Wire.TeamSubsystem import Wire.TeamSubsystem.Util import Wire.UserStore (UserStore) import Wire.UserStore qualified as Store +import Wire.UserSubsystem (UserSubsystem, internalUpdateSearchIndex) runAppSubsystem :: ( Member UserStore r, @@ -61,7 +62,8 @@ runAppSubsystem :: Member Now r, Member TeamSubsystem r, Member NotificationSubsystem r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserSubsystem r ) => Sem (AppSubsystem ': r) a -> Sem r a @@ -81,7 +83,8 @@ createAppImpl :: Member Now r, Member TeamSubsystem r, Member NotificationSubsystem r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserSubsystem r ) => Local UserId -> TeamId -> @@ -111,6 +114,7 @@ createAppImpl lusr tid (Apps.NewApp new password6) = do -- create app and user entries Store.createApp app Store.createUser u Nothing + internalUpdateSearchIndex u.id -- generate a team event generateTeamEvents creator.id tid [EdAppCreate u.id] From a258e5bce38105c58b57cd398d46b0e3cbd4f1e8 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 30 Dec 2025 21:54:34 +0100 Subject: [PATCH 38/60] WPB-16262 update nix packages (#4909) Co-authored-by: Akshay Mankar Co-authored-by: Sven Tennie Co-authored-by: Gautier DI FOLCO --- changelog.d/5-internal/WPB-16262 | 1 + hack/bin/generate-local-nix-packages.sh | 2 +- hack/bin/set-helm-chart-version.sh | 2 +- integration/test/Testlib/Certs.hs | 4 +- .../test/Testlib/MockIntegrationService.hs | 1 + libs/hscim/src/Web/Scim/Class/Group.hs | 2 +- .../Web/Scim/Schema/AuthenticationScheme.hs | 2 +- .../hscim/src/Web/Scim/Schema/User/Address.hs | 2 +- libs/hscim/src/Web/Scim/Schema/User/Email.hs | 2 +- libs/hscim/src/Web/Scim/Schema/User/Phone.hs | 2 +- libs/hscim/src/Web/Scim/Test/Util.hs | 2 +- .../src/HTTP2/Client/Manager/Internal.hs | 44 ++++++--- .../test/Test/HTTP2/Client/ManagerSpec.hs | 6 +- libs/imports/src/Imports.hs | 1 + .../src/SAML2/WebSSO/Test/Util/Misc.hs | 13 ++- libs/saml2-web-sso/src/Text/XML/DSig.hs | 1 - .../test/Test/SAML2/WebSSO/APISpec.hs | 2 +- libs/types-common-aws/src/Util/Test/SQS.hs | 2 - .../src/Data/CommaSeparatedList.hs | 2 +- libs/types-common/src/Data/Range.hs | 2 +- .../src/Network/Wai/Utilities/Exception.hs | 13 +++ .../src/Network/Wai/Utilities/Headers.hs | 2 +- .../src/Network/Wai/Utilities/Server.hs | 3 - libs/wai-utilities/wai-utilities.cabal | 1 + libs/wire-api-federation/default.nix | 2 - .../src/Wire/API/Federation/Client.hs | 25 +---- .../src/Wire/API/Federation/Error.hs | 13 +-- .../wire-api-federation.cabal | 1 - libs/wire-api/src/Wire/API/Connection.hs | 2 +- .../src/Wire/API/Conversation/CellsState.hs | 2 +- .../src/Wire/API/MLS/Group/Serialisation.hs | 5 +- .../src/Wire/API/Routes/Public/Util.hs | 1 + libs/wire-api/src/Wire/API/Team/Export.hs | 3 +- .../unit/Test/Wire/API/Routes/Version/Wai.hs | 2 +- libs/wire-subsystems/src/Wire/AWS.hs | 16 ++-- libs/wire-subsystems/src/Wire/Error.hs | 7 +- nix/haskell-pins.nix | 93 ++++++------------- nix/manual-overrides.nix | 17 +++- nix/sources.json | 6 +- nix/wire-server.nix | 11 ++- services/brig/src/Brig/AWS.hs | 11 +-- services/brig/src/Brig/Data/Client.hs | 7 +- .../brig/src/Brig/DeleteQueue/Interpreter.hs | 2 +- services/brig/src/Brig/Provider/RPC.hs | 3 +- services/brig/src/Brig/Queue/Stomp.hs | 2 +- services/brig/src/Brig/Run.hs | 2 +- .../cannon/src/Cannon/RabbitMqConsumerApp.hs | 2 +- services/cannon/src/Cannon/Run.hs | 2 +- services/cargohold/src/CargoHold/AWS.hs | 11 +-- services/cargohold/src/CargoHold/Run.hs | 2 +- services/cargohold/src/CargoHold/S3.hs | 4 - .../federator/src/Federator/MockServer.hs | 2 +- .../src/Federator/Monitor/Internal.hs | 9 +- services/galley/src/Galley/API/Federation.hs | 3 +- services/galley/src/Galley/API/Teams.hs | 8 +- services/galley/src/Galley/Keys.hs | 11 ++- services/galley/src/Galley/Run.hs | 2 +- services/gundeck/src/Gundeck/Aws.hs | 9 +- services/gundeck/src/Gundeck/Run.hs | 2 +- services/spar/src/Spar/Scim.hs | 3 +- services/wire-server-enterprise | 2 +- tools/db/find-undead/src/Main.hs | 2 +- tools/stern/src/Stern/Intra.hs | 7 +- 63 files changed, 209 insertions(+), 219 deletions(-) create mode 100644 changelog.d/5-internal/WPB-16262 create mode 100644 libs/wai-utilities/src/Network/Wai/Utilities/Exception.hs diff --git a/changelog.d/5-internal/WPB-16262 b/changelog.d/5-internal/WPB-16262 new file mode 100644 index 00000000000..75aeef95efa --- /dev/null +++ b/changelog.d/5-internal/WPB-16262 @@ -0,0 +1 @@ +Upgrade nixpkgs and dependencies (icluding GHC from 9.8 to 9.10) diff --git a/hack/bin/generate-local-nix-packages.sh b/hack/bin/generate-local-nix-packages.sh index 178f5515e65..cb12bd1ce19 100755 --- a/hack/bin/generate-local-nix-packages.sh +++ b/hack/bin/generate-local-nix-packages.sh @@ -4,7 +4,7 @@ set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &> /dev/null && pwd) -cabalFiles=$(find "$ROOT_DIR" -name '*.cabal' \ +cabalFiles=$(find "$ROOT_DIR" -type f -name '*.cabal' \ | grep -v dist-newstyle | sort) warningFile=$(mktemp) diff --git a/hack/bin/set-helm-chart-version.sh b/hack/bin/set-helm-chart-version.sh index b53b1857308..4a96a7ae9aa 100755 --- a/hack/bin/set-helm-chart-version.sh +++ b/hack/bin/set-helm-chart-version.sh @@ -22,7 +22,7 @@ function write_versions() { update_chart Chart.yaml # update all dependencies, if any - if [ -a requirements.yaml ]; then + if [ -e requirements.yaml ]; then sed -e "s/ version: \".*\"/ version: \"$target_version\"/g" requirements.yaml > "$tempfile" && mv "$tempfile" requirements.yaml for dep in $(helm dependency list | grep -v NAME | awk '{print $1}'); do if [ -d "$CHARTS_DIR/$dep" ] && [ "$chart" != "$dep" ]; then diff --git a/integration/test/Testlib/Certs.hs b/integration/test/Testlib/Certs.hs index 69d7b9c59b8..ed56b0b50e0 100644 --- a/integration/test/Testlib/Certs.hs +++ b/integration/test/Testlib/Certs.hs @@ -20,7 +20,7 @@ module Testlib.Certs where import Crypto.Hash.Algorithms (SHA256 (SHA256)) import qualified Crypto.PubKey.RSA as RSA import qualified Crypto.PubKey.RSA.PKCS15 as PKCS15 -import Crypto.Store.PKCS8 (PrivateKeyFormat (PKCS8Format), keyToPEM) +import Crypto.Store.PKCS8 (PrivateKeyFormat (PKCS8Format), keyPairFromPrivKey, keyToPEM) import Crypto.Store.X509 (pubKeyToPEM) import Data.ASN1.OID (OIDable (getObjectID)) import Data.Hourglass @@ -43,7 +43,7 @@ signedCertToString = toPem . PEM "CERTIFICATE" [] . encodeSignedObject -- | convert a private key to string privateKeyToString :: RSA.PrivateKey -> String -privateKeyToString = toPem . keyToPEM PKCS8Format . PrivKeyRSA +privateKeyToString = toPem . keyToPEM PKCS8Format . keyPairFromPrivKey . PrivKeyRSA -- | convert a public key to string publicKeyToString :: RSA.PublicKey -> String diff --git a/integration/test/Testlib/MockIntegrationService.hs b/integration/test/Testlib/MockIntegrationService.hs index 71dc2477cd3..7962fb4052f 100644 --- a/integration/test/Testlib/MockIntegrationService.hs +++ b/integration/test/Testlib/MockIntegrationService.hs @@ -37,6 +37,7 @@ import qualified Data.Aeson import qualified Data.ByteString.Lazy as LBS import Data.Streaming.Network import Data.String.Conversions (cs) +import Data.Type.Equality import Network.HTTP.Types import Network.Socket import qualified Network.Socket as Socket diff --git a/libs/hscim/src/Web/Scim/Class/Group.hs b/libs/hscim/src/Web/Scim/Class/Group.hs index 5ac96dbb122..16ef2d0140b 100644 --- a/libs/hscim/src/Web/Scim/Class/Group.hs +++ b/libs/hscim/src/Web/Scim/Class/Group.hs @@ -30,7 +30,7 @@ where import Data.Aeson import qualified Data.Aeson as Aeson -import Data.Text +import Data.Text hiding (show) import Servant import Servant.API.Generic import Servant.Server.Generic diff --git a/libs/hscim/src/Web/Scim/Schema/AuthenticationScheme.hs b/libs/hscim/src/Web/Scim/Schema/AuthenticationScheme.hs index 45c54868649..0b37f8d5712 100644 --- a/libs/hscim/src/Web/Scim/Schema/AuthenticationScheme.hs +++ b/libs/hscim/src/Web/Scim/Schema/AuthenticationScheme.hs @@ -25,7 +25,7 @@ module Web.Scim.Schema.AuthenticationScheme where import Data.Aeson -import Data.Text +import Data.Text hiding (show) import GHC.Generics import Network.URI.Static import Web.Scim.Schema.Common diff --git a/libs/hscim/src/Web/Scim/Schema/User/Address.hs b/libs/hscim/src/Web/Scim/Schema/User/Address.hs index 4d53badf500..1a725936701 100644 --- a/libs/hscim/src/Web/Scim/Schema/User/Address.hs +++ b/libs/hscim/src/Web/Scim/Schema/User/Address.hs @@ -18,7 +18,7 @@ module Web.Scim.Schema.User.Address where import Data.Aeson -import Data.Text hiding (dropWhile) +import Data.Text hiding (dropWhile, show) import GHC.Generics (Generic) import Web.Scim.Schema.Common diff --git a/libs/hscim/src/Web/Scim/Schema/User/Email.hs b/libs/hscim/src/Web/Scim/Schema/User/Email.hs index cd52a80a7e8..0b8bf7e919b 100644 --- a/libs/hscim/src/Web/Scim/Schema/User/Email.hs +++ b/libs/hscim/src/Web/Scim/Schema/User/Email.hs @@ -19,7 +19,7 @@ module Web.Scim.Schema.User.Email where import Control.Applicative ((<|>)) import Data.Aeson -import Data.Text hiding (dropWhile) +import Data.Text hiding (dropWhile, show) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import GHC.Generics (Generic) import qualified Text.Email.Validate as Email diff --git a/libs/hscim/src/Web/Scim/Schema/User/Phone.hs b/libs/hscim/src/Web/Scim/Schema/User/Phone.hs index 883a1b77140..5d4be4c4172 100644 --- a/libs/hscim/src/Web/Scim/Schema/User/Phone.hs +++ b/libs/hscim/src/Web/Scim/Schema/User/Phone.hs @@ -18,7 +18,7 @@ module Web.Scim.Schema.User.Phone where import Data.Aeson -import Data.Text hiding (dropWhile) +import Data.Text hiding (dropWhile, show) import GHC.Generics (Generic) import Web.Scim.Schema.Common diff --git a/libs/hscim/src/Web/Scim/Test/Util.hs b/libs/hscim/src/Web/Scim/Test/Util.hs index da75b438a47..d4ef837eeed 100644 --- a/libs/hscim/src/Web/Scim/Test/Util.hs +++ b/libs/hscim/src/Web/Scim/Test/Util.hs @@ -62,7 +62,7 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as L import Data.Proxy -import Data.Text +import Data.Text hiding (show) import Data.UUID as UUID import Data.UUID.V4 as UUID import GHC.Stack diff --git a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs index 1982979812a..5de12d91da9 100644 --- a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs +++ b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs @@ -43,7 +43,9 @@ import qualified Data.Text.Encoding as Text import Data.Unique import Foreign.Marshal.Alloc (mallocBytes) import GHC.IO.Exception +import qualified Network.HPACK as HPACK import qualified Network.HTTP2.Client as HTTP2 +import qualified Network.HTTP2.Client.Internal as HTTP2 import qualified Network.Socket as NS import qualified OpenSSL.Session as SSL import System.IO.Error @@ -157,9 +159,20 @@ sendRequestWithConnection conn req k = do result :: MVar r <- newEmptyMVar threadKilled :: MVar SomeException <- newEmptyMVar putMVar (connectionActionMVar conn) (SendRequest (Request req (putMVar result <=< k) threadKilled)) - race (takeMVar result) (takeMVar threadKilled) >>= \case + + let waitResult = takeMVar result + waitError = takeMVar threadKilled + waitDeath = do + res <- waitCatch (backgroundThread conn) + pure $ + case res of + Left e -> e + Right _ -> SomeException ConnectionAlreadyClosed + + race waitResult (race waitError waitDeath) >>= \case Left r -> pure r - Right (SomeException e) -> throw e + Right (Left e) -> throwIO e + Right (Right e) -> throwIO e -- | Make an HTTP2 request, if it is the first time the 'Http2Manager' sees this -- target, it creates the connection and keeps it around for @@ -338,12 +351,13 @@ startPersistentHTTP2ConnectionWithHook ctx (tlsEnabled, hostname, port) cl remov } -- Sends error to requests which show up too late, i.e. after the -- connection is already closed - tooLateNotifier e = forever $ do - takeMVar sendReqMVar >>= \case - SendRequest Request {..} -> do - -- No need to get stuck here - void $ tryPutMVar exceptionMVar (SomeException e) - CloseConnection -> pure () + tooLateNotifier e = do + forever $ do + takeMVar sendReqMVar >>= \case + SendRequest Request {..} -> do + -- No need to get stuck here + void $ tryPutMVar exceptionMVar (SomeException e) + CloseConnection -> pure () -- Sends errors to the request threads when an error occurs cleanupThreadsWith (SomeException e) = do @@ -360,6 +374,7 @@ startPersistentHTTP2ConnectionWithHook ctx (tlsEnabled, hostname, port) cl remov -- 1 second is hopefully enough to ensure that this thread is seen -- as finished. void $ async $ race_ (tooLateNotifier e) (threadDelay 1_000_000) + throwIO e hostnameForTLS = if removeTrailingDot @@ -466,11 +481,17 @@ data ConnectionAlreadyClosed = ConnectionAlreadyClosed instance Exception ConnectionAlreadyClosed -bufsize :: Int +bufsize :: HPACK.BufferSize bufsize = 4096 allocHTTP2Config :: Transport -> IO HTTP2.Config -allocHTTP2Config (InsecureTransport sock) = HTTP2.allocSimpleConfig sock bufsize +allocHTTP2Config (InsecureTransport sock) = do + res <- try $ HTTP2.allocSimpleConfig sock bufsize + case res of + Left (e :: SomeException) -> do + throwIO e + Right conf -> do + pure conf allocHTTP2Config (SecureTransport ssl) = do buf <- mallocBytes bufsize timmgr <- System.TimeManager.initialize $ 30 * 1000000 @@ -505,5 +526,6 @@ allocHTTP2Config (SecureTransport ssl) = do HTTP2.confPositionReadMaker = HTTP2.defaultPositionReadMaker, HTTP2.confTimeoutManager = timmgr, HTTP2.confMySockAddr = mysa, - HTTP2.confPeerSockAddr = peersa + HTTP2.confPeerSockAddr = peersa, + HTTP2.confReadNTimeout = False } diff --git a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs index 352f1c68fb0..15b99661f20 100644 --- a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs +++ b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs @@ -52,6 +52,7 @@ import qualified Network.HTTP2.Client as Client import qualified Network.HTTP2.Client as HTTP2 import Network.HTTP2.Server (defaultServerConfig) import qualified Network.HTTP2.Server as Server +import qualified Network.HTTP2.Server.Internal as Server import Network.Socket import qualified Network.Socket as NS import qualified OpenSSL.Session as SSL @@ -222,7 +223,7 @@ specTemplate mCtx = do -- to know what happens when we don't wait for the background thread to go -- away. Just deadConn <- Map.lookup (isJust mCtx, "localhost", port) <$> readTVarIO (connections mgr) - wait $ backgroundThread deadConn + void $ waitCatch $ backgroundThread deadConn withTestServerOnPort mCtx port $ \TestServer {..} -> do echoTest mgr (isJust mCtx) port @@ -325,7 +326,8 @@ allocServerConfig (Right ssl) = do Server.confPositionReadMaker = Server.defaultPositionReadMaker, Server.confTimeoutManager = timmgr, Server.confMySockAddr = mysa, - Server.confPeerSockAddr = peersa + Server.confPeerSockAddr = peersa, + HTTP2.confReadNTimeout = False } testServerOnSocket :: Maybe SSL.SSLContext -> Socket -> IORef Int -> IORef (Map Unique (Async ())) -> IO () diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index a0a1cfcb518..afa40e3c576 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -226,6 +226,7 @@ import Prelude ($!), (^), (^^), + type (~), ) import Prelude qualified as P diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/Test/Util/Misc.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/Test/Util/Misc.hs index 7760f80d2b1..9d511b92c42 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/Test/Util/Misc.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/Test/Util/Misc.hs @@ -24,11 +24,13 @@ import Control.Monad import Control.Monad.IO.Class import Data.ByteString.Base64.Lazy qualified as EL (encode) import Data.ByteString.Lazy qualified as LBS +import Data.Char (isSpace) import Data.EitherR import Data.Generics.Uniplate.Data import Data.List (sort) import Data.String import Data.String.Conversions +import Data.Text qualified as T import Data.Text.Lazy.IO qualified as LT import Data.Typeable import Data.UUID as UUID @@ -114,11 +116,11 @@ normalizeDocument = renderAndParse . transformBis [ [transformer $ \(Name nm nmspace _prefix) -> Name nm nmspace Nothing], - [transformer $ \(Element nm attrs nodes) -> Element nm attrs (sort . filter (not . isSignature) $ nodes)] + [transformer $ \(Element nm attrs nodes) -> Element nm attrs (sort . filter (not . isIgnorableNode) $ nodes)] ] renderAndParse :: (HasCallStack) => Document -> Document -renderAndParse doc = case parseText def $ renderText def {rsPretty = True} doc of +renderAndParse doc = case parseText def $ renderText def doc of Right doc' -> doc' bad@(Left _) -> error $ "impossible: " <> show bad @@ -126,6 +128,13 @@ isSignature :: Node -> Bool isSignature (NodeElement (Element name _ _)) = name == "{http://www.w3.org/2000/09/xmldsig#}Signature" isSignature _ = False +isIgnorableNode :: Node -> Bool +isIgnorableNode node = isSignature node || isWhitespaceOnly node + +isWhitespaceOnly :: Node -> Bool +isWhitespaceOnly (NodeContent txt) = T.all isSpace txt +isWhitespaceOnly _ = False + ---------------------------------------------------------------------- -- helpers diff --git a/libs/saml2-web-sso/src/Text/XML/DSig.hs b/libs/saml2-web-sso/src/Text/XML/DSig.hs index d42b9f38ac2..ecf48ff75ae 100644 --- a/libs/saml2-web-sso/src/Text/XML/DSig.hs +++ b/libs/saml2-web-sso/src/Text/XML/DSig.hs @@ -63,7 +63,6 @@ import Data.Either (isRight) import Data.EitherR (fmapL) import Data.Foldable (toList) import Data.Hourglass qualified as Hourglass -import Data.List (foldl') import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List.NonEmpty qualified as NL import Data.List.NonEmpty qualified as NonEmpty diff --git a/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs b/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs index 9b4ae65b546..10ef6681629 100644 --- a/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs +++ b/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs @@ -110,7 +110,7 @@ spec = describe "API" $ do <> "" <> "" Right (SomeSAMLRequest -> doc) = XML.parseText XML.def have - spuri = [uri|https://ServiceProvider.com/SAML/SLO/Browser/%%|] + spuri = [uri|https://ServiceProvider.com/SAML/SLO/Browser/%25%25|] (fmapL show . parseText def . cs $ mimeRender (Proxy @HTML) (FormRedirect spuri doc)) `shouldBe` Right want describe "simpleVerifyAuthnResponse" $ do diff --git a/libs/types-common-aws/src/Util/Test/SQS.hs b/libs/types-common-aws/src/Util/Test/SQS.hs index f9d015a1631..7e96760b4cf 100644 --- a/libs/types-common-aws/src/Util/Test/SQS.hs +++ b/libs/types-common-aws/src/Util/Test/SQS.hs @@ -161,8 +161,6 @@ parseDeleteMessage url m = do sendEnv :: ( MonadReader AWS.Env m, MonadResource m, - Typeable a, - Typeable (AWS.AWSResponse a), AWS.AWSRequest a ) => a -> diff --git a/libs/types-common/src/Data/CommaSeparatedList.hs b/libs/types-common/src/Data/CommaSeparatedList.hs index fa4f07396f2..ec73778e9d8 100644 --- a/libs/types-common/src/Data/CommaSeparatedList.hs +++ b/libs/types-common/src/Data/CommaSeparatedList.hs @@ -29,7 +29,7 @@ import Data.Range (Bounds, Range) import Data.Text qualified as Text import Data.Text.Encoding (decodeUtf8With, encodeUtf8) import Data.Text.Encoding.Error -import Imports +import Imports hiding (List) import Servant (FromHttpApiData (..), ToHttpApiData (toQueryParam)) newtype CommaSeparatedList a = CommaSeparatedList {fromCommaSeparatedList :: [a]} diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index c9adb0bc213..ed74973b2e7 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -92,7 +92,7 @@ import Data.Text.Lazy qualified as TL import Data.Type.Bool import Data.Type.Ord import GHC.TypeNats -import Imports +import Imports hiding (List) import Servant (FromHttpApiData (..)) import System.Random (Random) import Test.QuickCheck (Arbitrary (arbitrary, shrink), Gen) diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Exception.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Exception.hs new file mode 100644 index 00000000000..45f43e1cefe --- /dev/null +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Exception.hs @@ -0,0 +1,13 @@ +module Network.Wai.Utilities.Exception where + +import Control.Exception +import Imports + +-- | `displayException` with empty `ExceptionContext` +-- +-- Starting with GHC 9.10, exceptions carry a context that contains backtraces. +-- Displaying these is not always desired; e.g. for HTTP response bodies. +displayExceptionNoBacktrace :: (Exception e) => e -> String +displayExceptionNoBacktrace = trim . displayException . toException + where + trim = (dropWhileEnd isSpace) . (dropWhile isSpace) diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs index 56049d0ecdf..f994dd996cb 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs @@ -20,7 +20,7 @@ module Network.Wai.Utilities.Headers where import Data.ByteString import Data.ByteString.Conversion (FromByteString (..), ToByteString (..), fromByteString', toByteString') import Data.OpenApi.ParamSchema (ToParamSchema (..)) -import Data.Text as T +import Data.Text as T hiding (show) import Data.Text.Encoding import Data.Text.Encoding.Error import Imports diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index 2442bd62c37..b5e32d35201 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -67,7 +67,6 @@ import Data.UUID qualified as UUID import Data.UUID.V4 qualified as UUID import Imports import Network.HTTP.Types -import Network.HTTP2.Internal import Network.Wai import Network.Wai.Handler.Warp import Network.Wai.Handler.Warp.Internal (TimeoutThread) @@ -211,8 +210,6 @@ errorHandlers = _ -> pure . Left $ Wai.mkError status500 "server-error" "Server Error", - -- similar to ThreadKilled, but this is thrown by the HTTP2 client - Handler $ \(x :: KilledByHttp2ThreadManager) -> throwIO x, Handler $ \(_ :: InvalidRequest) -> pure . Left $ Wai.mkError status400 "client-error" "Invalid Request", diff --git a/libs/wai-utilities/wai-utilities.cabal b/libs/wai-utilities/wai-utilities.cabal index 559ce68e460..9faf8a2ed40 100644 --- a/libs/wai-utilities/wai-utilities.cabal +++ b/libs/wai-utilities/wai-utilities.cabal @@ -65,6 +65,7 @@ library exposed-modules: Network.Wai.Utilities Network.Wai.Utilities.Error + Network.Wai.Utilities.Exception Network.Wai.Utilities.Headers Network.Wai.Utilities.JSONResponse Network.Wai.Utilities.MockServer diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index b39e2071bf0..272b4e73de1 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -6,7 +6,6 @@ , aeson , aeson-pretty , amqp -, async , base , bytestring , bytestring-conversion @@ -54,7 +53,6 @@ mkDerivation { libraryHaskellDepends = [ aeson amqp - async base bytestring bytestring-conversion diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs index d706935aff7..b4e76e41b85 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -35,7 +35,6 @@ module Wire.API.Federation.Client ) where -import Control.Concurrent.Async import Control.Exception qualified as E import Control.Monad.Catch import Control.Monad.Codensity @@ -65,7 +64,6 @@ import Network.HTTP.Media qualified as HTTP import Network.HTTP.Types qualified as HTTP import Network.HTTP2.Client qualified as HTTP2 import Network.Wai.Utilities.Error qualified as Wai -import OpenSSL.Session qualified as SSL import Servant.Client import Servant.Client.Core import Servant.Types.SourceT @@ -128,27 +126,13 @@ liftCodensity = FederatorClient . lift . lift . lift headersFromTable :: HTTP2.TokenHeaderTable -> [HTTP.Header] headersFromTable (headerList, _) = flip map headerList $ first HTTP2.tokenKey --- This opens a new http2 connection. Using a http2-manager leads to this problem https://wearezeta.atlassian.net/browse/WPB-4787 --- FUTUREWORK: Replace with H2Manager.withHTTP2Request once the bugs are solved. -withNewHttpRequest :: H2Manager.Target -> HTTP2.Request -> (HTTP2.Response -> IO a) -> IO a -withNewHttpRequest target req k = do - ctx <- SSL.context - let cacheLimit = 20 - sslRemoveTrailingDot = False - tcpConnectionTimeout = 30_000_000 - sendReqMVar <- newEmptyMVar - thread <- liftIO . async $ H2Manager.startPersistentHTTP2Connection ctx target cacheLimit sslRemoveTrailingDot tcpConnectionTimeout sendReqMVar - let newConn = H2Manager.HTTP2Conn thread (putMVar sendReqMVar H2Manager.CloseConnection) sendReqMVar - H2Manager.sendRequestWithConnection newConn req \resp -> - k resp `finally` newConn.disconnect - performHTTP2Request :: Http2Manager -> H2Manager.Target -> HTTP2.Request -> IO (Either FederatorClientHTTP2Error (ResponseF Builder)) -performHTTP2Request _mgr target req = try $ do - withNewHttpRequest target req $ consumeStreamingResponseWith $ \resp -> do +performHTTP2Request mgr target req = try $ do + H2Manager.withHTTP2Request mgr target req $ consumeStreamingResponseWith $ \resp -> do b <- fmap (fromRight mempty) . runExceptT @@ -186,8 +170,7 @@ instance (KnownComponent c) => RunClient (FederatorClient c) where { requestHeaders = ( versionHeader, toByteString' - ( versionInt (fromMaybe V0 v) - ) + (versionInt (fromMaybe V0 v)) ) :<| requestHeaders req } @@ -255,7 +238,7 @@ withHTTP2StreamingRequest successfulStatus req handleResponse = do $ Codensity $ \k -> E.catches - (withNewHttpRequest (False, hostname, port) req' (consumeStreamingResponseWith (k . Right))) + (H2Manager.withHTTP2Request (ceHttp2Manager env) (False, hostname, port) req' (consumeStreamingResponseWith (k . Right))) [ E.Handler $ k . Left . FederatorClientHTTP2Error, E.Handler $ k . Left . FederatorClientHTTP2Error . FederatorClientConnectionError, E.Handler $ k . Left . FederatorClientHTTP2Error . FederatorClientHTTP2Exception, diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs index b80d873552f..3fee0083d71 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -95,6 +95,7 @@ import Network.HTTP.Types.Status import Network.HTTP.Types.Status qualified as HTTP import Network.HTTP2.Client qualified as HTTP2 import Network.Wai.Utilities.Error qualified as Wai +import Network.Wai.Utilities.Exception import OpenSSL.Session (SomeSSLException) import Servant.Client import Wire.API.Error @@ -227,21 +228,21 @@ federationRemoteHTTP2Error target path = \case ( Wai.mkError unexpectedFederationResponseStatus "federation-http2-error" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) ) & addErrData (FederatorClientTLSException e) -> ( Wai.mkError (HTTP.mkStatus 525 "SSL Handshake Failure") "federation-tls-error" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) ) & addErrData (FederatorClientConnectionError e) -> ( Wai.mkError federatorConnectionRefusedStatus "federation-connection-refused" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) ) & addErrData where @@ -259,12 +260,12 @@ federationClientHTTP2Error (FederatorClientConnectionError e) = Wai.mkError HTTP.status500 "federation-not-available" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) federationClientHTTP2Error e = Wai.mkError HTTP.status500 "federation-local-error" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) federationRemoteResponseError :: SrvTarget -> Text -> HTTP.Status -> LByteString -> Wai.Error federationRemoteResponseError target path status body = @@ -310,7 +311,7 @@ federationServantErrorToWai (UnsupportedContentType mediaType res) = <> LT.pack (show mediaType) ) federationServantErrorToWai (ConnectionError e) = - federationUnavailable . T.pack . displayException $ e + federationUnavailable . T.pack . displayExceptionNoBacktrace $ e federationErrorContentType :: ResponseF a -> LT.Text federationErrorContentType = diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index e39c5af0969..00a2f9dd718 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -86,7 +86,6 @@ library build-depends: aeson >=2.0.1.0 , amqp - , async , base >=4.6 && <5.0 , bytestring , bytestring-conversion diff --git a/libs/wire-api/src/Wire/API/Connection.hs b/libs/wire-api/src/Wire/API/Connection.hs index cf56bd1f451..d7843692c5e 100644 --- a/libs/wire-api/src/Wire/API/Connection.hs +++ b/libs/wire-api/src/Wire/API/Connection.hs @@ -50,7 +50,7 @@ import Data.OpenApi qualified as S import Data.Qualified (Qualified (qUnqualified), Remote, deprecatedSchema) import Data.Range import Data.Schema -import Data.Text as Text +import Data.Text as Text hiding (show) import Imports import Servant.API import Wire.API.Routes.MultiTablePaging diff --git a/libs/wire-api/src/Wire/API/Conversation/CellsState.hs b/libs/wire-api/src/Wire/API/Conversation/CellsState.hs index 5ecee42ae83..63b73576ab4 100644 --- a/libs/wire-api/src/Wire/API/Conversation/CellsState.hs +++ b/libs/wire-api/src/Wire/API/Conversation/CellsState.hs @@ -84,4 +84,4 @@ instance HasCellsState CellsState where getCellsState = id instance HasCellsState () where - getCellsState = def + getCellsState _ = def diff --git a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs index 9b5671791da..3be4adf6c66 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs @@ -39,6 +39,7 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.UUID qualified as UUID import Imports +import Network.Wai.Utilities.Exception import Web.HttpApiData (FromHttpApiData (parseHeader)) import Wire.API.Conversation hiding (newGroupId) import Wire.API.MLS.Group @@ -116,7 +117,7 @@ getParts = do eDomain <- T.decodeUtf8' . L.toStrict <$> getRemainingLazyByteString - domain <- either (fail . displayException) pure eDomain + domain <- either (fail . displayExceptionNoBacktrace) pure eDomain pure GroupIdParts { convType, @@ -148,7 +149,7 @@ getDomain = do len <- fromIntegral <$> getWord16be domain <- T.decodeUtf8' <$> getByteString len case domain of - Left e -> fail (displayException e) + Left e -> fail (displayExceptionNoBacktrace e) Right d -> pure (Domain d) newGroupId :: ConvType -> Qualified ConvOrSubConvId -> GroupId diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Util.hs b/libs/wire-api/src/Wire/API/Routes/Public/Util.hs index 5517bb6635e..014fc647f34 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Util.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Util.hs @@ -23,6 +23,7 @@ module Wire.API.Routes.Public.Util where import Control.Comonad import Data.Maybe import Data.SOP (I (..), NS (..)) +import Imports import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.MultiVerb diff --git a/libs/wire-api/src/Wire/API/Team/Export.hs b/libs/wire-api/src/Wire/API/Team/Export.hs index c31040c5e42..156f541e5ed 100644 --- a/libs/wire-api/src/Wire/API/Team/Export.hs +++ b/libs/wire-api/src/Wire/API/Team/Export.hs @@ -35,6 +35,7 @@ import Data.Time.Clock import Data.Time.Format import Data.Vector (fromList) import Imports +import Network.Wai.Utilities.Exception import Test.QuickCheck import Wire.API.Team.Role (Role) import Wire.API.User (AccountStatus (..), Name) @@ -150,7 +151,7 @@ parseByteString bstr = parseUTCTime :: ByteString -> Parser UTCTime parseUTCTime b = do - s <- either (fail . displayException) pure $ T.decodeUtf8' b + s <- either (fail . displayExceptionNoBacktrace) pure $ T.decodeUtf8' b parseTimeM False defaultTimeLocale timestampFormat (T.unpack s) parseAccountStatus :: ByteString -> Parser AccountStatus diff --git a/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs index b0f253cba72..7583d717378 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs @@ -20,7 +20,7 @@ module Test.Wire.API.Routes.Version.Wai where import Data.Proxy import Data.Set qualified as Set import Data.String.Conversions -import Data.Text as T +import Data.Text as T hiding (show) import Imports import Network.HTTP.Types.Status (status200, status400) import Network.Wai diff --git a/libs/wire-subsystems/src/Wire/AWS.hs b/libs/wire-subsystems/src/Wire/AWS.hs index 45e49680f0d..78b7e74eff6 100644 --- a/libs/wire-subsystems/src/Wire/AWS.hs +++ b/libs/wire-subsystems/src/Wire/AWS.hs @@ -22,6 +22,7 @@ module Wire.AWS where import Amazonka qualified as AWS import Amazonka.SQS qualified as SQS import Amazonka.SQS.Lens qualified as SQS +import Control.Exception.Lens import Control.Lens hiding ((.=)) import Control.Monad.Catch import Control.Monad.Trans.Resource @@ -123,7 +124,7 @@ mkEnv lgr mgr endpoint qname = do getQueueUrl e q = do x <- runResourceT $ - AWS.trying AWS._Error $ + trying AWS._Error $ AWS.send e (SQS.newGetQueueUrl q) either (throwM . GeneralError) @@ -150,26 +151,21 @@ enqueue ev = do sendCatch :: ( Member (Embed IO) r, Member (Input AWS.Env) r, - AWS.AWSRequest req, - Typeable req, - Typeable (AWS.AWSResponse req) + AWS.AWSRequest req ) => req -> Sem r (Either AWS.Error (AWS.AWSResponse req)) sendCatch req = do env <- input - embed $ runResourceT (AWS.trying AWS._Error (AWS.send env req)) + embed $ runResourceT (trying AWS._Error (AWS.send env req)) -- Amazon monad variant sendCatchEnv :: - ( AWS.AWSRequest r, - Typeable r, - Typeable (AWS.AWSResponse r) - ) => + (AWS.AWSRequest r) => AWS.Env -> r -> Amazon (Either AWS.Error (AWS.AWSResponse r)) -sendCatchEnv e = AWS.trying AWS._Error . AWS.send e +sendCatchEnv e = trying AWS._Error . AWS.send e canRetry :: Either AWS.Error a -> Bool canRetry (Right _) = False diff --git a/libs/wire-subsystems/src/Wire/Error.hs b/libs/wire-subsystems/src/Wire/Error.hs index d5505e9ae2e..b526bb6c9da 100644 --- a/libs/wire-subsystems/src/Wire/Error.hs +++ b/libs/wire-subsystems/src/Wire/Error.hs @@ -35,6 +35,7 @@ import Network.HTTP.Types import Network.Wai import Network.Wai.Utilities import Network.Wai.Utilities.Error qualified as Wai +import Network.Wai.Utilities.Exception import Network.Wai.Utilities.JSONResponse import Network.Wai.Utilities.Server import Servant (ServerError (..)) @@ -86,9 +87,9 @@ postgresUsageErrorToHttpError err = case err of -- return "404 not found", not "database crashed"? -- The problem is that the SessionError is not typed to easily be parsed -- To prevent foreign key errors we should check the foreign key constraints before inserting - StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err)) - ConnectionUsageError _ -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err)) - AcquisitionTimeoutUsageError -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err)) + StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> displayExceptionNoBacktrace err)) + ConnectionUsageError _ -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> displayExceptionNoBacktrace err)) + AcquisitionTimeoutUsageError -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> displayExceptionNoBacktrace err)) -- | Extract the wai error from an HttpError and convert into a -- servant error. `RichError` extra data is discarded! diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 8184e905762..273ba19bb32 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -147,6 +147,8 @@ let # missing upstream PR, this will get removed when completing # servantification + # + # this is currently still used/needed in the proxy service wai-predicates = { src = fetchgit { url = "https://github.com/wireapp/wai-predicates"; @@ -218,49 +220,19 @@ let src = pkgs.fetchFromGitHub { owner = "yesodweb"; repo = "wai"; - rev = "8b20c9db265a202a2c7ba2a9ec8786a1ee59957b"; - hash = "sha256-fKUSiRl38FKY1gFSmbksktoqoLfQrDxRRWEh4k+RRW4="; + rev = "ef34334b160c74b62435ccc21f5b458f73506b2f"; + hash = "sha256-7rgZUimPJY+0yVN717pZ2Ep01+XB0z8C/+L9D3Qz9/k="; }; }; - # this contains an important fix to the initialization of the window size - # and should be switched to upstream as soon as we can - # version = "5.2.5"; - # This patch also includes suppressing ConnectionIsClosed http2 = { src = fetchgit { url = "https://github.com/wireapp/http2"; - rev = "45653e3caab0642e539fab2681cb09402aae29ca"; - hash = "sha256-L90PQtDw/JFwyltSVFvmfjTAb0ZLhFt9Hl0jbzn+cFQ="; - }; - }; - - # hs-opentelemetry-* has not been released for a while on hackage. Thus, - # we're following main. - hs-opentelemetry = { - src = fetchgit { - url = "https://github.com/iand675/hs-opentelemetry"; - rev = "ee8a6dad7db306eb67748ddcd77df4974ad8259e"; - hash = "sha256-UirBRxY9gAv5x/t87RZcWCy6GtsigzFMABKqrhS9b7s="; - }; - packages = { - hs-opentelemetry-sdk = "sdk"; - hs-opentelemetry-api = "api"; - hs-opentelemetry-propagator-datadog = "propagators/datadog"; - hs-opentelemetry-instrumentation-http-client = "instrumentation/http-client"; - hs-opentelemetry-instrumentation-wai = "instrumentation/wai"; - hs-opentelemetry-exporter-otlp = "exporters/otlp"; - hs-opentelemetry-utils-exceptions = "utils/exceptions"; + rev = "ca606d86ed304fa780f7a60d11244019c62a10e0"; + hash = "sha256-eyjFtB28JCcvItZ5R8CT2F5GL62c49oQ49AN8/4HSYw="; }; }; - HaskellNet = { - src = fetchgit { - url = "https://github.com/wireapp/HaskellNet"; - rev = "74cde03b4beb09794a6120ea5321a09430bcd2c7"; - hash = "sha256-VIM60sXCVC25ULf/2yPvqANK/h9BY6dEYY3o3/xiEEQ="; - }; - }; # Our fork of 2.0.0. This release hasn't been updated for a while and Nix # is bad in coping with Hackage patched revisions and overriding @@ -269,16 +241,20 @@ let # N.B. only the listed packages work. If you want to use another: # - list it here # - patch it on the fork (if required) + # + # Can't currently be removed because amazonka-dynamodb-attributevalue + # does not exist on hackage amazonka = { src = fetchgit { - url = "https://github.com/wireapp/amazonka"; - rev = "d98cefc04bcc7076a915076a322ab5905c6a4945"; - hash = "sha256-8HNHoTUaLi5lyOrKYybacZsDSHrju9/oo+Lf/YulbIo="; + url = "https://github.com/brendanhay/amazonka"; + rev = "a7d699be1076e2aad05a1930ca3937ffea954ad8"; + hash = "sha256-cCRhHH/IgM7tPy8rXHTSRec1zxohO8NWxSVZEG1OjQw="; }; packages = { amazonka = "lib/amazonka"; amazonka-core = "lib/amazonka-core"; amazonka-dynamodb = "lib/services/amazonka-dynamodb"; + amazonka-dynamodb-attributevalue = "lib/amazonka-dynamodb-attributevalue"; amazonka-s3 = "lib/services/amazonka-s3"; amazonka-sts = "lib/services/amazonka-sts"; amazonka-sqs = "lib/services/amazonka-sqs"; @@ -294,41 +270,18 @@ let hackagePins = { # start pinned dependencies for http2 http-semantics = { - version = "0.1.2"; - sha256 = "sha256-S4rGBCIKVPpLPumLcVzrPONrbWm8VBizqxI3dXNIfr0="; - }; - - tasty-ant-xml = { - version = "1.1.9"; - sha256 = "sha256-aB7B61XSAZ5V+uW+QBe/PKBmhdFfX3OoOjDE9jB7Mek="; + version = "0.4.0"; + sha256 = "sha256-rh0z51EKvsu5rQd5n2z3fSRjjEObouNZSBPO9NFYOF0="; }; network-run = { - version = "0.3.0"; - sha256 = "sha256-FP2GZKwacC+TLLwEIVgKBtnKplYPf5xOIjDfvlbQV0o="; - }; - time-manager = { - version = "0.1.0"; - sha256 = "sha256-WRe9LZrOIPJVBFk0vMN2IMoxgP0a0psQCiCiOFWJc74="; - }; - hasql = { - version = "1.9.1.2"; - sha256 = "sha256-W2pAC3wLIizmbspWHeWDQqb5AROtwA8Ok+lfZtzTlQg="; - }; - - hasql-pool = { - version = "1.3.0.1"; - sha256 = "sha256-TtNrs1z8L39WnX8277V97g9Ot1DwutKLrAB1JOjQQoQ="; + version = "0.5.0"; + sha256 = "sha256-vbXh+CzxDsGApjqHxCYf/ijpZtUCApFbkcF5gyN0THU="; }; - postgresql-syntax = { - version = "0.4.1.3"; - sha256 = "sha256-afC4lQUPUL5cHe+7vTG1lFZ4wWyQzdh9MEhMT/TtP5c="; - }; - - network-control = { - version = "0.1.0"; - sha256 = "sha256-D6pKb6+0Pr08FnObGbXBVMv04ys3N731p7U+GYH1oEg="; + time-manager = { + version = "0.2.4"; + sha256 = "sha256-sAt/331YLQ2IU3z90aKYSq1nxoazv87irsuJp7ZG3pw="; }; # end pinned dependencies for http2 @@ -339,6 +292,12 @@ let version = "2.0.0.0"; sha256 = "sha256-SQyFjl1Zf4vnntjZHJpf46gMR3LXWCQAMsR56NdsvRA="; }; + + # Pin uri-bytestring: newer parser rejects unescaped Set-Cookie in SSO mobile redirect query, breaking Spar’s URI substitution; stick to 0.3.3.1 for now + uri-bytestring = { + version = "0.3.3.1"; + sha256 = "sha256-jgSTBBDcxRQ0tjs0wTyvEpEAkGA7npJKjdXDT81VpT4="; + }; }; # Name -> Source -> Maybe Subpath -> Drv mkGitDrv = name: src: subpath: diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 189db41faa6..5ae39056828 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -14,7 +14,7 @@ hself: hsuper: { # tests need network access, cabal2nix disables haddocks cql-io = hlib.doHaddock (hlib.dontCheck hsuper.cql-io); - quickcheck-state-machine = hlib.dontCheck hsuper.quickcheck-state-machine; + quickcheck-state-machine = hlib.markUnbroken (hlib.dontCheck hsuper.quickcheck-state-machine); # Tests require a running redis hedis = hlib.dontCheck hsuper.hedis; @@ -25,7 +25,7 @@ hself: hsuper: { hasql = hlib.dontCheck hsuper.hasql; hasql-pool = hlib.dontCheck hsuper.hasql-pool; hasql-migration = hlib.markUnbroken (hlib.dontCheck hsuper.hasql-migration); - hasql-transaction = hlib.dontCheck hsuper.hasql-transaction_1_2_0_1; + hasql-transaction = hlib.dontCheck hsuper.hasql-transaction; # users 1.2.1 from nixpkgs postgresql-binary = hlib.dontCheck (hsuper.postgresql-binary); # --------------------- @@ -37,6 +37,10 @@ hself: hsuper: { bytestring-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.bytestring-arbitrary); lens-datetime = hlib.markUnbroken (hlib.doJailbreak hsuper.lens-datetime); postie = hlib.doJailbreak hsuper.postie; + lrucaching = hlib.doJailbreak (hlib.markUnbroken hsuper.lrucaching); + # added servant-openapi3 because the version bounds of some dependent packages + # of our pin exclude the versions in our current nixpkgs + servant-openapi3 = hlib.doJailbreak (hlib.dontCheck hsuper.servant-openapi3); # the libsodium haskell library is incompatible with the new version of the libsodium c library # that nixpkgs has - this downgrades libsodium from 1.0.19 to 1.0.18 @@ -53,13 +57,18 @@ hself: hsuper: { } ))); + # hs-opentelemetry pin removal bumps API -> 0.3.0.0 and SDK -> 0.1.0.1 from the pinned commit; instrumentation stays at 0.1.1.0/0.1.0.1. + hs-opentelemetry-instrumentation-wai = hlib.markUnbroken (hlib.doJailbreak hsuper.hs-opentelemetry-instrumentation-wai); + hs-opentelemetry-instrumentation-conduit = hlib.markUnbroken (hlib.doJailbreak hsuper.hs-opentelemetry-instrumentation-conduit); + hs-opentelemetry-instrumentation-http-client = hlib.doJailbreak hsuper.hs-opentelemetry-instrumentation-http-client; + hs-opentelemetry-utils-exceptions = hlib.markUnbroken (hlib.doJailbreak hsuper.hs-opentelemetry-utils-exceptions); + # ------------------------------------ # okay but marked broken (nixpkgs bug) # (we can unfortunately not do anything here but update nixpkgs) # ------------------------------------ template = hlib.markUnbroken hsuper.template; system-linux-proc = hlib.markUnbroken hsuper.system-linux-proc; - lrucaching = hlib.markUnbroken hsuper.lrucaching; # ----------------- # version overrides @@ -72,7 +81,7 @@ hself: hsuper: { Cabal = hsuper.Cabal_3_12_1_0; Cabal-syntax = hsuper.Cabal-syntax_3_14_2_0; - text-builder = hlib.doJailbreak (hsuper.text-builder_1_0_0_4); + text-builder = hlib.doJailbreak hsuper.text-builder; # uses 1.0.0.4 from nixpkgs # ----------------- # flags and patches diff --git a/nix/sources.json b/nix/sources.json index 45723e16396..cc357e99b48 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -5,10 +5,10 @@ "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c53baa6685261e5253a1c355a1b322f82674a824", - "sha256": "07iy9la8yc56477q0rh6bmkcgnm57n267g19i2si5yb2j320gpj7", + "rev": "09b8fda8959d761445f12b55f380d90375a1d6bb", + "sha256": "0333ri3rmkwlsyvbf8916psydq5i2xq0cj6iis9d6f4ghr19vbva", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/c53baa6685261e5253a1c355a1b322f82674a824.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/09b8fda8959d761445f12b55f380d90375a1d6bb.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 5063f8c4f84..999ec485d6b 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -422,7 +422,9 @@ let pkgs.kubelogin-oidc pkgs.nixpkgs-fmt pkgs.openssl - pkgs.ormolu + # FUTUREWORK: we are temporarily using the old ormolu version to prevent the noise of reformatting a lot of code + # upgrade ormolu and reformat in a follow up PR + pkgs.haskell.packages.ghc98.ormolu pkgs.vacuum-go pkgs.shellcheck pkgs.treefmt @@ -439,10 +441,11 @@ let # nicely in docker.nix at the root of https://github.com/nixos/nix. We get # this file using "${pkgs.nix.src}/docker.nix" so we don't have to also pin # the nix repository along with the nixpkgs repository. - ciImage = import "${pkgs.nix.src}/docker.nix" { + ciImage = import "${pkgs.nixVersions.latest.src}/docker.nix" { inherit pkgs; name = "quay.io/wire/wire-server-ci"; maxLayers = 2; + nix = pkgs.nixVersions.latest; # We don't need to push the "latest" tag, every step in CI should depend # deterministically on a specific image. tag = null; @@ -498,12 +501,12 @@ in pkgs.bash pkgs.crate2nix pkgs.dash - (pkgs.haskell-language-server.override { supportedGhcVersions = [ "98" ]; }) + (pkgs.haskell-language-server.override { supportedGhcVersions = [ "910" ]; }) pkgs.ghcid pkgs.kind pkgs.netcat pkgs.niv - pkgs.haskellPackages.apply-refact + pkgs.haskell.packages.ghc912.apply-refact (pkgs.python3.withPackages (ps: with ps; [ black diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index 1345b4d4cb0..e304440ce25 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -49,6 +49,7 @@ import Amazonka.SES qualified as SES import Amazonka.SQS qualified as SQS import Amazonka.SQS.Lens qualified as SQS import Brig.Options qualified as Opt +import Control.Exception.Lens import Control.Lens hiding ((.=)) import Control.Monad.Catch import Control.Monad.Trans.Resource @@ -201,14 +202,14 @@ enqueueFIFO url group dedup m = retrying retry5x (const $ pure . canRetry) (cons -- Utilities send :: - (AWSRequest r, Typeable r, Typeable (AWSResponse r)) => + (AWSRequest r) => r -> Amazon (AWSResponse r) send r = throwA =<< sendCatchAmazon r -- | Temporary helper to translate polysemy to Amazon monad, it should go away -- with more polysemisation -sendCatchAmazon :: (AWSRequest req, Typeable req, Typeable (AWSResponse req)) => req -> Amazon (Either AWS.Error (AWS.AWSResponse req)) +sendCatchAmazon :: (AWSRequest req) => req -> Amazon (Either AWS.Error (AWS.AWSResponse req)) sendCatchAmazon req = do env <- view amazonkaEnv liftIO . runM . runInputConst env $ sendCatch req @@ -218,9 +219,7 @@ throwA = either (throwM . GeneralError) pure execCatch :: ( AWSRequest a, - Typeable a, MonadUnliftIO m, - Typeable (AWSResponse a), MonadCatch m ) => AWS.Env -> @@ -228,13 +227,11 @@ execCatch :: m (Either AWS.Error (AWSResponse a)) execCatch e cmd = runResourceT $ - AWS.trying AWS._Error $ + trying AWS._Error $ AWS.send e cmd exec :: ( AWSRequest a, - Typeable a, - Typeable (AWSResponse a), MonadCatch m, MonadIO m ) => diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 5dc3a526fc9..b94e0dd00df 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -117,8 +117,7 @@ reAuthForNewClients :: ReAuthPolicy reAuthForNewClients count upsert = count > 0 && not upsert addClient :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => Local UserId -> ClientId -> NewClient -> @@ -554,7 +553,7 @@ withOptLock u c ma = go (10 :: Int) Prom.incCounter optimisticLockFailedCounter execDyn :: forall r x. - (AWS.AWSRequest r, Typeable r, Typeable (AWS.AWSResponse r)) => + (AWS.AWSRequest r) => (AWS.AWSResponse r -> Maybe x) -> (Text -> r) -> m (Maybe x) @@ -565,7 +564,7 @@ withOptLock u c ma = go (10 :: Int) where execDyn' :: forall y p. - (AWS.AWSRequest p, Typeable (AWS.AWSResponse p), Typeable p) => + (AWS.AWSRequest p) => AWS.Env -> (AWS.AWSResponse p -> Maybe y) -> p -> diff --git a/services/brig/src/Brig/DeleteQueue/Interpreter.hs b/services/brig/src/Brig/DeleteQueue/Interpreter.hs index 8b78c9c5ef4..6c644839652 100644 --- a/services/brig/src/Brig/DeleteQueue/Interpreter.hs +++ b/services/brig/src/Brig/DeleteQueue/Interpreter.hs @@ -29,7 +29,7 @@ import Control.Lens import Data.Aeson import Data.ByteString.Base16 qualified as B16 import Data.ByteString.Lazy qualified as BL -import Data.Text as T +import Data.Text as T hiding (show) import Data.Text.Encoding qualified as T import Imports import OpenSSL.EVP.Digest hiding (digest) diff --git a/services/brig/src/Brig/Provider/RPC.hs b/services/brig/src/Brig/Provider/RPC.hs index f01d8cbcab9..c039bc7f5c9 100644 --- a/services/brig/src/Brig/Provider/RPC.hs +++ b/services/brig/src/Brig/Provider/RPC.hs @@ -46,6 +46,7 @@ import Imports import Network.HTTP.Client qualified as Http import Network.HTTP.Types.Method import Network.HTTP.Types.Status +import Network.Wai.Utilities.Exception import Ssl.Util (withVerifiedSslConnection) import System.Logger.Class (MonadLogger, field, msg, val, (~~)) import System.Logger.Class qualified as Log @@ -98,7 +99,7 @@ createBot scon new = do extReq scon ["bots"] . method POST . Bilge.json new - onExc ex = lift (extLogError scon ex) >> throwE (ServiceUnavailableWith $ displayException ex) + onExc ex = lift (extLogError scon ex) >> throwE (ServiceUnavailableWith $ displayExceptionNoBacktrace ex) extReq :: ServiceConn -> [ByteString] -> Request -> Request extReq scon ps = diff --git a/services/brig/src/Brig/Queue/Stomp.hs b/services/brig/src/Brig/Queue/Stomp.hs index 631f790013a..f92f5be658f 100644 --- a/services/brig/src/Brig/Queue/Stomp.hs +++ b/services/brig/src/Brig/Queue/Stomp.hs @@ -34,7 +34,7 @@ import Control.Retry hiding (retryPolicy) import Data.Aeson as Aeson import Data.ByteString.Lazy qualified as BL import Data.Conduit.Network.TLS -import Data.Text +import Data.Text hiding (show) import Data.Text.Encoding import Network.Mom.Stompl.Client.Queue hiding (try) import System.Logger.Class as Log diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 77db6d20f27..f1a1ea4a926 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -133,7 +133,7 @@ mkApp opts = do . requestIdMiddleware e.appLogger defaultRequestIdHeaderName . Metrics.servantPrometheusMiddleware (Proxy @ServantCombinedAPI) . GZip.gunzip - . GZip.gzip GZip.def + . GZip.gzip GZip.defaultGzipSettings . catchErrors e.appLogger defaultRequestIdHeaderName servantApp :: Env -> Wai.Application diff --git a/services/cannon/src/Cannon/RabbitMqConsumerApp.hs b/services/cannon/src/Cannon/RabbitMqConsumerApp.hs index 16c3c0e53fd..13f7f1950cb 100644 --- a/services/cannon/src/Cannon/RabbitMqConsumerApp.hs +++ b/services/cannon/src/Cannon/RabbitMqConsumerApp.hs @@ -30,7 +30,7 @@ import Control.Lens hiding ((#)) import Control.Monad.Codensity import Data.Aeson hiding (Key) import Data.Id -import Data.Text +import Data.Text hiding (show) import Data.Text qualified as Text import Data.Text.Lazy qualified as TL import Data.Text.Lazy.Encoding qualified as TLE diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index 1583072882d..42faacdd3db 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -101,7 +101,7 @@ run o = lowerCodensity $ do . requestIdMiddleware g defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) . otelMiddleWare - . Gzip.gzip Gzip.def + . Gzip.gzip Gzip.defaultGzipSettings . catchErrors g defaultRequestIdHeaderName app :: Application app = middleware (serve (Proxy @CombinedAPI) server) diff --git a/services/cargohold/src/CargoHold/AWS.hs b/services/cargohold/src/CargoHold/AWS.hs index 4984561ba43..bc3b60ccc20 100644 --- a/services/cargohold/src/CargoHold/AWS.hs +++ b/services/cargohold/src/CargoHold/AWS.hs @@ -41,6 +41,7 @@ import CargoHold.API.Error import CargoHold.CloudFront import CargoHold.Options hiding (cloudFront, s3Bucket) import Conduit +import Control.Exception.Lens import Control.Lens hiding ((.=)) import Control.Monad.Catch import Control.Retry @@ -151,16 +152,14 @@ instance Exception Error -- Utilities sendCatch :: - (MonadCatch m, AWSRequest r, MonadResource m, Typeable r, Typeable (AWSResponse r)) => + (MonadCatch m, AWSRequest r, MonadResource m) => AWS.Env -> r -> m (Either AWS.Error (AWSResponse r)) -sendCatch env = AWS.trying AWS._Error . AWS.send env +sendCatch env = trying AWS._Error . AWS.send env exec :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r, MonadLogger m, MonadIO m, @@ -190,8 +189,6 @@ rethrowError e = case e of execStream :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r ) => Env -> @@ -213,8 +210,6 @@ execStream env request = do execCatch :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r, MonadLogger m, MonadIO m diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index beeba087634..77eb3628cf4 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -94,7 +94,7 @@ mkApp o = Codensity $ \k -> versionMiddleware (foldMap expandVersionExp o.settings.disabledAPIVersions) . requestIdMiddleware e.appLogger defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) - . GZip.gzip GZip.def + . GZip.gzip GZip.defaultGzipSettings . catchErrors e.appLogger defaultRequestIdHeaderName servantApp :: Env -> Application servantApp e0 r cont = do diff --git a/services/cargohold/src/CargoHold/S3.hs b/services/cargohold/src/CargoHold/S3.hs index 08bcdda6594..79126f484d3 100644 --- a/services/cargohold/src/CargoHold/S3.hs +++ b/services/cargohold/src/CargoHold/S3.hs @@ -427,8 +427,6 @@ octets = MIME.Type (MIME.Application "octet-stream") [] exec :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r ) => (Text -> r) -> @@ -439,8 +437,6 @@ exec req = do execCatch :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r ) => (Text -> r) -> diff --git a/services/federator/src/Federator/MockServer.hs b/services/federator/src/Federator/MockServer.hs index f3a7eb05c6e..828a87bcb1a 100644 --- a/services/federator/src/Federator/MockServer.hs +++ b/services/federator/src/Federator/MockServer.hs @@ -137,7 +137,7 @@ mockInternalRequest :: mockInternalRequest remoteCalls mock targetDomain component (RPC path) req cont = do domainTxt <- note NoOriginDomain $ lookup originDomainHeaderName (Wai.requestHeaders req) originDomain <- parseDomain domainTxt - reqBody <- embed $ Wai.lazyRequestBody req + reqBody <- embed $ Wai.strictRequestBody req let fedRequest = ( FederatedRequest { frOriginDomain = originDomain, diff --git a/services/federator/src/Federator/Monitor/Internal.hs b/services/federator/src/Federator/Monitor/Internal.hs index 6f37abdc80f..d696c6e18e5 100644 --- a/services/federator/src/Federator/Monitor/Internal.hs +++ b/services/federator/src/Federator/Monitor/Internal.hs @@ -28,6 +28,7 @@ import Federator.Options (RunSettings (..)) import GHC.Foreign (peekCStringLen, withCStringLen) import GHC.IO.Encoding (getFileSystemEncoding) import Imports +import Network.Wai.Utilities.Exception import OpenSSL.Session (SSLContext) import OpenSSL.Session qualified as SSL import Polysemy (Embed, Member, Members, Sem, embed) @@ -332,7 +333,7 @@ showFederationSetupError (InvalidCAStore path msg) = "invalid CA store: " <> Tex showFederationSetupError (InvalidClientCertificate msg) = Text.pack msg showFederationSetupError (InvalidClientPrivateKey msg) = Text.pack msg showFederationSetupError (CertificateAndPrivateKeyDoNotMatch cert key) = Text.pack $ "Certificate and private key do not match, certificate: " <> cert <> ", private key: " <> key -showFederationSetupError (SSLException exc) = Text.pack $ "Unexpected SSL Exception: " <> displayException exc +showFederationSetupError (SSLException exc) = Text.pack $ "Unexpected SSL Exception: " <> displayExceptionNoBacktrace exc mkSSLContext :: ( Member (Embed IO) r, @@ -343,10 +344,10 @@ mkSSLContext :: mkSSLContext settings = do ctx <- mkSSLContextWithoutCert settings - Polysemy.fromExceptionVia @SomeException (InvalidClientCertificate . displayException) $ + Polysemy.fromExceptionVia @SomeException (InvalidClientCertificate . displayExceptionNoBacktrace) $ SSL.contextSetCertificateChainFile ctx (clientCertificate settings) - Polysemy.fromExceptionVia @SomeException (InvalidClientPrivateKey . displayException) $ + Polysemy.fromExceptionVia @SomeException (InvalidClientPrivateKey . displayExceptionNoBacktrace) $ SSL.contextSetPrivateKeyFile ctx (clientPrivateKey settings) privateKeyCheck <- Polysemy.fromExceptionVia @SSL.SomeSSLException SSLException $ SSL.contextCheckPrivateKey ctx @@ -378,7 +379,7 @@ mkSSLContextWithoutCert settings = do SSL.vpCallback = Nothing } forM_ (remoteCAStore settings) $ \caStorePath -> - Polysemy.fromExceptionVia @SomeException (InvalidCAStore caStorePath . displayException) $ + Polysemy.fromExceptionVia @SomeException (InvalidCAStore caStorePath . displayExceptionNoBacktrace) $ SSL.contextSetCAFile ctx caStorePath when (useSystemCAStore settings) $ diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 25920337ae0..b6212622b23 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -57,6 +57,7 @@ import Galley.Effects import Galley.Options import Galley.Types.Conversations.One2One import Imports +import Network.Wai.Utilities.Exception import Polysemy import Polysemy.Error import Polysemy.Input @@ -339,7 +340,7 @@ leaveConversation requestingDomain lc = do pure $ LeaveConversationResponse (Right ()) where - internalErr = InternalErrorWithDescription . LT.pack . displayException + internalErr = InternalErrorWithDescription . LT.pack . displayExceptionNoBacktrace -- FUTUREWORK: report errors to the originating backend -- FUTUREWORK: error handling for missing / mismatched clients diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 041011c9d60..df79cc4504b 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -61,8 +61,8 @@ import Brig.Types.Team (TeamSize (..)) import Cassandra (PageWithState (pwsResults), pwsHasMore) import Cassandra qualified as C import Control.Lens -import Data.ByteString.Conversion (List, toByteString) -import Data.ByteString.Conversion qualified +import Data.ByteString.Conversion (toByteString) +import Data.ByteString.Conversion qualified as BSC import Data.ByteString.Lazy qualified as LBS import Data.Default import Data.HashMap.Strict qualified as HM @@ -1040,7 +1040,7 @@ setSearchVisibility availableForTeam luid tid req = do withTeamIds :: (Member TeamStore r, Member (ListItems LegacyPaging TeamId) r) => UserId -> - Maybe (Either (Range 1 32 (List TeamId)) TeamId) -> + Maybe (Either (Range 1 32 (BSC.List TeamId)) TeamId) -> Range 1 100 Int32 -> (Bool -> [TeamId] -> Sem r a) -> Sem r a @@ -1052,7 +1052,7 @@ withTeamIds usr range size k = case range of r <- E.listItems usr (Just c) (rcast size) k (resultSetType r == ResultSetTruncated) (resultSetResult r) Just (Left (fromRange -> cc)) -> do - ids <- E.selectTeams usr (Data.ByteString.Conversion.fromList cc) + ids <- E.selectTeams usr (BSC.fromList cc) k False ids {-# INLINE withTeamIds #-} diff --git a/services/galley/src/Galley/Keys.hs b/services/galley/src/Galley/Keys.hs index 5cbfa540b21..e11faa1454a 100644 --- a/services/galley/src/Galley/Keys.hs +++ b/services/galley/src/Galley/Keys.hs @@ -38,6 +38,7 @@ import Data.PEM import Data.Proxy import Data.X509 import Imports +import Network.Wai.Utilities.Exception import Wire.API.MLS.CipherSuite import Wire.API.MLS.Keys @@ -119,7 +120,7 @@ decodeEcdsaKeyPair bytes = do pem <- expectOne "private key" pems let content = pemContent pem -- parse outer pkcs8 container as BER - asn1 <- first displayException (decodeASN1' BER content) + asn1 <- first displayExceptionNoBacktrace (decodeASN1' BER content) (oid, key) <- case asn1 of [ Start Sequence, IntVal _version, @@ -139,7 +140,7 @@ decodeEcdsaKeyPair bytes = do ) $ guard (oid == curveOID @c) -- parse key bytestring as BER again, this should be in the format of rfc5915 - asn1' <- first displayException (decodeASN1' BER key) + asn1' <- first displayExceptionNoBacktrace (decodeASN1' BER key) (privBS, pubBS) <- case asn1' of [ Start Sequence, IntVal _version, @@ -151,10 +152,10 @@ decodeEcdsaKeyPair bytes = do ] -> pure (priv, pub) _ -> Left "invalid ECDSA key format: expected rfc5915 private key format" priv <- - first displayException . eitherCryptoError $ + first displayExceptionNoBacktrace . eitherCryptoError $ ECDSA.decodePrivate curve privBS pub <- - first displayException . eitherCryptoError $ + first displayExceptionNoBacktrace . eitherCryptoError $ ECDSA.decodePublic curve pubBS pure (priv, pub) @@ -165,7 +166,7 @@ decodeEd25519PrivateKey bytes = do pems <- pemParseLBS bytes pem <- expectOne "private key" pems let content = pemContent pem - asn1 <- first displayException (decodeASN1' BER content) + asn1 <- first displayExceptionNoBacktrace (decodeASN1' BER content) (priv, remainder) <- fromASN1 asn1 expectEmpty remainder case priv of diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index ca34cc1212c..326d0bbfa83 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -102,7 +102,7 @@ mkApp opts = . servantPrometheusMiddleware (Proxy @CombinedAPI) . otelMiddleware . GZip.gunzip - . GZip.gzip GZip.def + . GZip.gzip GZip.defaultGzipSettings . catchErrors logger defaultRequestIdHeaderName Codensity \k -> k () `finally` do diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index 71014205f40..c31f95d019b 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -65,6 +65,7 @@ import Amazonka.SQS.Lens qualified as SQS import Amazonka.SQS.Types import Control.Category ((>>>)) import Control.Error hiding (err, isRight) +import Control.Exception.Lens import Control.Lens hiding ((.=)) import Control.Monad.Catch import Control.Monad.Trans.Resource @@ -208,7 +209,7 @@ mkEnv lgr opts mgr = do getQueueUrl e q = do x <- runResourceT $ - AWS.trying AWS._Error $ + trying AWS._Error $ AWS.send e (SQS.newGetQueueUrl q) either (throwM . GeneralError) @@ -473,14 +474,14 @@ listen throttleMillis callback = do -- Utilities sendCatch :: - (AWSRequest r, Typeable r, Typeable (AWSResponse r)) => + (AWSRequest r) => AWS.Env -> r -> Amazon (Either AWS.Error (AWSResponse r)) -sendCatch env = AWS.trying AWS._Error . AWS.send env +sendCatch env = trying AWS._Error . AWS.send env send :: - (AWSRequest r, Typeable r, Typeable (AWSResponse r)) => + (AWSRequest r) => AWS.Env -> r -> Amazon (AWSResponse r) diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index f3f1ed140db..89e4c9f8ef2 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -156,7 +156,7 @@ run opts = withTracer \tracer -> do . requestIdMiddleware (env ^. applog) defaultRequestIdHeaderName . Metrics.servantPrometheusMiddleware (Proxy @(GundeckAPI :<|> InternalAPI)) . GZip.gunzip - . GZip.gzip GZip.def + . GZip.gzip GZip.defaultGzipSettings . catchErrors (env ^. applog) defaultRequestIdHeaderName mkApp :: Env -> Wai.Application diff --git a/services/spar/src/Spar/Scim.hs b/services/spar/src/Spar/Scim.hs index 18060c4d64b..e4d020a85f7 100644 --- a/services/spar/src/Spar/Scim.hs +++ b/services/spar/src/Spar/Scim.hs @@ -67,6 +67,7 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Text.Encoding.Error import Imports +import Network.Wai.Utilities.Exception import Polysemy import Polysemy.Error (Error, fromExceptionSem, runError, throw, try) import Polysemy.Input (Input) @@ -160,7 +161,7 @@ apiScim = -- We caught an exception that's not a Spar exception at all. It is wrapped into -- Scim.serverError. throw . SAML.CustomError . SparScimError $ - Scim.serverError (T.pack (displayException someException)) + Scim.serverError (T.pack (displayExceptionNoBacktrace someException)) Right (Left err@(SAML.CustomError (SparScimError _))) -> -- We caught a 'SparScimError' exception. It is left as-is. throw err diff --git a/services/wire-server-enterprise b/services/wire-server-enterprise index 255cf1a0341..b4419041fc3 160000 --- a/services/wire-server-enterprise +++ b/services/wire-server-enterprise @@ -1 +1 @@ -Subproject commit 255cf1a034143ffd6a408012356ab350ede4cd97 +Subproject commit b4419041fc31ca260accec9d4f0f019bfd53c077 diff --git a/tools/db/find-undead/src/Main.hs b/tools/db/find-undead/src/Main.hs index 5bc9506308e..ac4eaa00cfc 100644 --- a/tools/db/find-undead/src/Main.hs +++ b/tools/db/find-undead/src/Main.hs @@ -24,7 +24,7 @@ where import Cassandra as C import Cassandra.Settings as C -import Data.Text as Text +import Data.Text as Text hiding (show) import Database.Bloodhound qualified as ES import Imports import Network.HTTP.Client qualified as HTTP diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index b81c11fb597..bae73c12ca9 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -81,7 +81,7 @@ import Data.Aeson hiding (Error) import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.Types (emptyArray) import Data.ByteString.Char8 qualified as BS -import Data.ByteString.Conversion +import Data.ByteString.Conversion as BSC import Data.ByteString.UTF8 qualified as UTF8 import Data.Domain import Data.Handle (Handle) @@ -101,6 +101,7 @@ import Network.HTTP.Types (urlEncode) import Network.HTTP.Types.Method import Network.HTTP.Types.Status hiding (statusCode, statusMessage) import Network.Wai.Utilities (Error (..), mkError) +import Network.Wai.Utilities.Exception import Servant.API import Servant.Client qualified as SC import Servant.Server qualified as SS @@ -196,7 +197,7 @@ getUserConnections uid = do parseResponse (mkError status502 "bad-upstream") r batchSize = 100 :: Int -getUsersConnections :: List UserId -> Handler [ConnectionStatus] +getUsersConnections :: BSC.List UserId -> Handler [ConnectionStatus] getUsersConnections uids = do info $ msg "Getting user connections" b <- asks (.brig) @@ -1063,7 +1064,7 @@ runClientToHandler :: SC.ClientM a -> Handler a runClientToHandler client = do clientEnv <- asks (.brigServantClientEnv) res <- liftIO $ SC.runClientM client clientEnv - either (throwE . mkError status400 "servant-client-error" . LT.pack . displayException) pure res + either (throwE . mkError status400 "servant-client-error" . LT.pack . displayExceptionNoBacktrace) pure res domRegLock :: Domain -> SC.ClientM NoContent domRegUnlock :: Domain -> SC.ClientM NoContent From 924787d3ded8df1ade4c4cf0e9d9b551234f9112 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Wed, 31 Dec 2025 17:20:46 +0100 Subject: [PATCH 39/60] Upgrade GHC from 9.8 to 9.10 (#4597) * WPB-22506: replace `show` with `displayException`/`displayExceptionNoBacktrace` * WPB-22128: Migrate displayException calls Drop the backtraces when the resulting string is used in HTTP response bodies. --------- Co-authored-by: Gautier DI FOLCO --- libs/bilge/bilge.cabal | 1 + libs/bilge/default.nix | 2 ++ libs/bilge/src/Bilge/Assert.hs | 3 ++- libs/bilge/src/Bilge/RPC.hs | 3 ++- libs/saml2-web-sso/default.nix | 2 ++ libs/saml2-web-sso/saml2-web-sso.cabal | 1 + libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs | 5 +++-- libs/saml2-web-sso/src/Text/XML/DSig.hs | 15 ++++++++------- .../src/Wire/ExternalAccess/External.hs | 2 +- .../Wire/TeamInvitationSubsystem/Interpreter.hs | 3 ++- .../src/Wire/BackgroundWorker/Jobs/Registry.hs | 4 ++-- services/brig/src/Brig/AWS.hs | 2 +- services/brig/src/Brig/Queue/Stomp.hs | 2 +- services/brig/src/Brig/Run.hs | 2 +- services/cannon/src/Cannon/App.hs | 4 ++-- services/cargohold/src/CargoHold/AWS.hs | 6 +++--- services/federator/src/Federator/Interpreter.hs | 2 +- .../federator/test/unit/Test/Federator/Client.hs | 2 +- .../federator/test/unit/Test/Federator/Options.hs | 6 +++--- services/galley/src/Galley/API/Query.hs | 5 ++--- .../Galley/External/LegalHoldService/Internal.hs | 2 +- .../test/integration/API/Teams/LegalHold/Util.hs | 5 ++--- services/gundeck/src/Gundeck/Push/Native.hs | 2 +- services/gundeck/src/Gundeck/Push/Websocket.hs | 4 ++-- services/gundeck/src/Gundeck/Redis.hs | 6 +++--- .../gundeck/src/Gundeck/ThreadBudget/Internal.hs | 2 +- services/proxy/src/Proxy/API/Public.hs | 2 +- services/spar/src/Spar/Sem/SAML2/Library.hs | 2 +- services/spar/src/Spar/Sem/Utils.hs | 2 +- tools/entreprise-provisioning/default.nix | 2 ++ .../entreprise-provisioning.cabal | 1 + tools/entreprise-provisioning/src/API.hs | 5 +++-- tools/stern/src/Stern/API.hs | 7 ++++--- tools/stern/src/Stern/Intra.hs | 2 +- 34 files changed, 65 insertions(+), 51 deletions(-) diff --git a/libs/bilge/bilge.cabal b/libs/bilge/bilge.cabal index 8e64bbe92e5..9aefddd77d4 100644 --- a/libs/bilge/bilge.cabal +++ b/libs/bilge/bilge.cabal @@ -98,6 +98,7 @@ library , uri-bytestring , wai , wai-extra + , wai-utilities , wire-otel default-language: GHC2021 diff --git a/libs/bilge/default.nix b/libs/bilge/default.nix index 1844d50b1d2..0f90d357e62 100644 --- a/libs/bilge/default.nix +++ b/libs/bilge/default.nix @@ -26,6 +26,7 @@ , uri-bytestring , wai , wai-extra +, wai-utilities , wire-otel }: mkDerivation { @@ -54,6 +55,7 @@ mkDerivation { uri-bytestring wai wai-extra + wai-utilities wire-otel ]; description = "Library for composing HTTP requests"; diff --git a/libs/bilge/src/Bilge/Assert.hs b/libs/bilge/src/Bilge/Assert.hs index a13e624aa51..5c669040225 100644 --- a/libs/bilge/src/Bilge/Assert.hs +++ b/libs/bilge/src/Bilge/Assert.hs @@ -43,6 +43,7 @@ import Data.ByteString qualified as S import Data.ByteString.Lazy qualified as Lazy import Imports import Network.HTTP.Client +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import System.Console.ANSI import Text.Printf @@ -98,7 +99,7 @@ io m a - printErr e = error $ title "Error executing request: " ++ err (show e) + printErr e = error $ title "Error executing request: " ++ err (displayExceptionNoBacktrace e) -- | Like ' Msg -> Msg rpcExceptionMsg (RPCException sys req ex) = - "remote" .= sys ~~ "path" .= HTTP.path req ~~ headers ~~ msg (show ex) + "remote" .= sys ~~ "path" .= HTTP.path req ~~ headers ~~ msg (displayExceptionNoBacktrace ex) where headers = foldr hdr id (HTTP.requestHeaders req) hdr (k, v) x = x ~~ original k .= v diff --git a/libs/saml2-web-sso/default.nix b/libs/saml2-web-sso/default.nix index cf699cf0738..e4d714b7145 100644 --- a/libs/saml2-web-sso/default.nix +++ b/libs/saml2-web-sso/default.nix @@ -72,6 +72,7 @@ , uuid , wai , wai-extra +, wai-utilities , warp , word8 , xml-conduit @@ -150,6 +151,7 @@ mkDerivation { uuid wai wai-extra + wai-utilities warp word8 xml-conduit diff --git a/libs/saml2-web-sso/saml2-web-sso.cabal b/libs/saml2-web-sso/saml2-web-sso.cabal index 4d05fc179b5..da09daed9ed 100644 --- a/libs/saml2-web-sso/saml2-web-sso.cabal +++ b/libs/saml2-web-sso/saml2-web-sso.cabal @@ -145,6 +145,7 @@ library , uuid >=1.3.13 , wai >=3.2.2.1 , wai-extra >=3.0.28 + , wai-utilities , warp >=3.2.28 , word8 >=0.1.3 , xml-conduit >=1.8.0.1 diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs index bfff13e20ad..e3b77b281f3 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs @@ -49,6 +49,7 @@ import Data.Typeable (Proxy (Proxy), Typeable) import Data.X509 qualified as X509 import GHC.Stack import Network.URI qualified as URI +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import SAML2.Bindings.Identifiers qualified as HX import SAML2.Core qualified as HX import SAML2.Metadata.Metadata qualified as HX @@ -86,7 +87,7 @@ encode = Text.XML.renderText settings . renderToDocument settings = def {rsNamespaces = nameSpaces (Proxy @a), rsXMLDeclaration = True} decode :: forall m a. (HasXMLRoot a, MonadError String m) => LT -> m a -decode = either (throwError . show @SomeException) parseFromDocument . parseText def +decode = either (throwError . displayExceptionNoBacktrace @SomeException) parseFromDocument . parseText def encodeElem :: forall a. (HasXML a) => a -> LT encodeElem = Text.XML.renderText settings . mkDocument' . render @@ -96,7 +97,7 @@ encodeElem = Text.XML.renderText settings . mkDocument' . render mkDocument' bad = error $ "encodeElem: " <> show bad decodeElem :: forall a m. (HasXML a, MonadError String m) => LT -> m a -decodeElem = either (throwError . show @SomeException) parseFromDocument . parseText def +decodeElem = either (throwError . displayExceptionNoBacktrace @SomeException) parseFromDocument . parseText def renderToDocument :: (HasXMLRoot a) => a -> Document renderToDocument = mkDocument . renderRoot diff --git a/libs/saml2-web-sso/src/Text/XML/DSig.hs b/libs/saml2-web-sso/src/Text/XML/DSig.hs index ecf48ff75ae..e5cbd50c176 100644 --- a/libs/saml2-web-sso/src/Text/XML/DSig.hs +++ b/libs/saml2-web-sso/src/Text/XML/DSig.hs @@ -72,6 +72,7 @@ import Data.UUID as UUID import Data.X509 qualified as X509 import GHC.Stack import Network.URI (URI (..), parseRelativeReference) +import Network.Wai.Utilities.Exception import SAML2.XML qualified as HS hiding (Node, URI) import SAML2.XML.Canonical qualified as HS import SAML2.XML.Signature qualified as HS @@ -145,7 +146,7 @@ parseKeyInfo doVerify (cs @LT @LBS -> lbs) = case HS.xmlToSAML @HS.KeyInfo =<< s -- | Call 'stripWhitespaceDoc' on a rendered bytestring. stripWhitespaceLBS :: (m ~ Either String) => LBS -> m LBS -stripWhitespaceLBS lbs = renderLBS def . stripWhitespace <$> fmapL show (parseLBS def lbs) +stripWhitespaceLBS lbs = renderLBS def . stripWhitespace <$> fmapL displayExceptionNoBacktrace (parseLBS def lbs) renderKeyInfo :: (HasCallStack) => X509.SignedCertificate -> LT renderKeyInfo cert = cs . ourSamlToXML . HS.KeyInfo Nothing $ NonEmpty.singleton (HS.X509Data (NonEmpty.singleton (HS.X509Certificate cert))) @@ -224,8 +225,8 @@ mkSignCredsWithCert mValidSince size = do verify :: forall m. (MonadError String m) => NonEmpty SignCreds -> LBS -> String -> m HXTC.XmlTree verify creds el sid = case unsafePerformIO (try @SomeException $ verifyIO creds el sid) of Right (_, Right xml) -> pure xml - Right (_, Left exc) -> throwError $ show exc - Left exc -> throwError $ show exc + Right (_, Left signErr) -> throwError $ show signErr + Left exc -> throwError $ displayExceptionNoBacktrace exc -- | Convenient wrapper that picks the ID of the root element node and passes it to `verify`. verifyRoot :: forall m. (MonadError String m) => NonEmpty SignCreds -> LBS -> m HXTC.XmlTree @@ -233,7 +234,7 @@ verifyRoot creds el = do signedID <- do XML.Document _ (XML.Element _ attrs _) _ <- either - (throwError . ("Could not parse signed document: " <>) . cs . show) + (throwError . ("Could not parse signed document: " <>) . cs . displayExceptionNoBacktrace) pure (XML.parseLBS XML.def el) maybe @@ -272,7 +273,7 @@ verifySignatureUnenvelopedSigs :: HS.PublicKeys -> String -> HXTC.XmlTree -> IO verifySignatureUnenvelopedSigs pks xid doc = catchAll $ warpResult <$> verifySignature pks xid doc where catchAll :: IO (Either HS.SignatureError a) -> IO (Either HS.SignatureError a) - catchAll = handle $ pure . Left . HS.SignatureVerificationLegacyFailure . Left . (show @SomeException) + catchAll = handle $ pure . Left . HS.SignatureVerificationLegacyFailure . Left . (displayExceptionNoBacktrace @SomeException) warpResult :: Maybe HXTC.XmlTree -> Either HS.SignatureError HXTC.XmlTree warpResult (Just xml) = Right xml @@ -413,7 +414,7 @@ signRootAt sigPos (SignPrivCreds hashAlg (SignPrivKeyRSA keypair)) doc = } ] docCanonic :: SBS <- - either (throwError . show) (pure . cs) . unsafePerformIO . try @SomeException $ + either (throwError . displayExceptionNoBacktrace) (pure . cs) . unsafePerformIO . try @SomeException $ HS.applyTransforms transforms (HXT.mkRoot [] [docInHXT]) let digest :: SBS digest = case hashAlg of @@ -437,7 +438,7 @@ signRootAt sigPos (SignPrivCreds hashAlg (SignPrivKeyRSA keypair)) doc = -- (note that there are two rounds of SHA256 application, hence two mentions of the has alg here) signedInfoSBS :: SBS <- - either (throwError . show) (pure . cs) . unsafePerformIO . try @SomeException $ + either (throwError . displayExceptionNoBacktrace) (pure . cs) . unsafePerformIO . try @SomeException $ HS.applyCanonicalization (HS.signedInfoCanonicalizationMethod signedInfo) Nothing $ HS.samlToDoc signedInfo sigval :: SBS <- diff --git a/libs/wire-subsystems/src/Wire/ExternalAccess/External.hs b/libs/wire-subsystems/src/Wire/ExternalAccess/External.hs index cd0e67465ff..768f37f141f 100644 --- a/libs/wire-subsystems/src/Wire/ExternalAccess/External.hs +++ b/libs/wire-subsystems/src/Wire/ExternalAccess/External.hs @@ -188,7 +188,7 @@ deliver env pp = mapM (Async.async . exec) pp >>= foldM evaluate [] . zip (map f field "provider" (toByteString (s ^. serviceRefProvider)) ~~ field "service" (toByteString (s ^. serviceRefId)) ~~ field "bot" (toByteString (botMemId b)) - ~~ field "error" (show ex) + ~~ field "error" (displayException ex) ~~ msg (val "External delivery failure") pure gone Nothing -> do diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs index 58e01782d4b..2f6ff578364 100644 --- a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs @@ -27,6 +27,7 @@ import Data.Set qualified as Set import Data.Text.Ascii qualified as AsciiText import Data.Text.Encoding qualified as Text import Imports +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import Polysemy import Polysemy.Error import Polysemy.Input (Input, input, runInputConst) @@ -265,7 +266,7 @@ logInvitationRequest context action = runError action >>= \case Left e -> do Log.warn $ - msg @String ("Failed to create invitation: " <> show e) + msg @String ("Failed to create invitation: " <> displayExceptionNoBacktrace e) . context throw e Right res@(_, code) -> do diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index e9e6240ada4..1c9b3416bd5 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -78,9 +78,9 @@ dispatchJob job = do . interpretRace . runDelay . runError - . mapError @FederationError (T.pack . show) + . mapError @FederationError (T.pack . displayException) . mapError @UsageError (T.pack . show) - . mapError @ParseException (T.pack . show) + . mapError @ParseException (T.pack . displayException) . mapError @MigrationError (T.pack . show) . interpretTinyLog env job.requestId job.jobId . runInputConst env.hasqlPool diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index e304440ce25..5bcbf90ecd5 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -182,7 +182,7 @@ listen throttleMillis url callback = forever . handleAny unexpectedError $ do liftIO $ callback n for_ (m ^. SQS.message_receiptHandle) (void . send . SQS.newDeleteMessage url) unexpectedError x = do - err $ "error" .= show x ~~ msg (val "Failed to read or process message from SQS") + err $ "error" .= displayException x ~~ msg (val "Failed to read or process message from SQS") threadDelay 3000000 enqueueStandard :: Text -> BL.ByteString -> Amazon SQS.SendMessageResponse diff --git a/services/brig/src/Brig/Queue/Stomp.hs b/services/brig/src/Brig/Queue/Stomp.hs index f92f5be658f..8fa9b04336a 100644 --- a/services/brig/src/Brig/Queue/Stomp.hs +++ b/services/brig/src/Brig/Queue/Stomp.hs @@ -166,7 +166,7 @@ listen b q callback = Log.err $ msg (val "Exception when listening to a STOMP queue") ~~ field "queue" (show q) - ~~ field "error" (show e) + ~~ field "error" (displayException e) pure True -- Note [exception handling] diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index f1a1ea4a926..5b5d0f96818 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -241,7 +241,7 @@ pendingActivationCleanup = do safeForever funName action = forever $ action `catchAny` \exc -> do - err $ "error" .= show exc ~~ msg (val $ UTF8.fromString funName <> " failed") + err $ "error" .= displayException exc ~~ msg (val $ UTF8.fromString funName <> " failed") -- pause to keep worst-case noise in logs manageable threadDelay 60_000_000 diff --git a/services/cannon/src/Cannon/App.hs b/services/cannon/src/Cannon/App.hs index 2ad956a087c..ed7daa0784c 100644 --- a/services/cannon/src/Cannon/App.hs +++ b/services/cannon/src/Cannon/App.hs @@ -159,8 +159,8 @@ rejectOnError p x = do ioErrors :: (MonadLogger m) => Key -> [Handler m ()] ioErrors k = let f s = Logger.err $ client (key2bytes k) . msg s - in [ Handler $ \(x :: HandshakeException) -> f (show x), - Handler $ \(x :: IOException) -> f (show x) + in [ Handler $ \(x :: HandshakeException) -> f (displayException x), + Handler $ \(x :: IOException) -> f (displayException x) ] ping :: Message diff --git a/services/cargohold/src/CargoHold/AWS.hs b/services/cargohold/src/CargoHold/AWS.hs index bc3b60ccc20..83cbb168dc4 100644 --- a/services/cargohold/src/CargoHold/AWS.hs +++ b/services/cargohold/src/CargoHold/AWS.hs @@ -175,7 +175,7 @@ exec env request = do Left err -> do Logger.info env.logger $ Log.field "remote" (Log.val "S3") - ~~ Log.msg (show err) + ~~ Log.msg (displayException err) ~~ Log.msg (show req) -- We re-throw the error, but distinguish between user errors and server -- errors. Logging it here also gives us the request that caused it. @@ -201,7 +201,7 @@ execStream env request = do Left err -> do Logger.info env.logger $ Log.field "remote" (Log.val "S3") - ~~ Log.msg (show err) + ~~ Log.msg (displayException err) ~~ Log.msg (show req) -- We just re-throw the error, but logging it here also gives us the request -- that caused it. @@ -224,7 +224,7 @@ execCatch env request = do Left err -> do Log.info $ Log.field "remote" (Log.val "S3") - ~~ Log.msg (show err) + ~~ Log.msg (displayException err) ~~ Log.msg (show req) pure Nothing Right r -> pure $ Just r diff --git a/services/federator/src/Federator/Interpreter.hs b/services/federator/src/Federator/Interpreter.hs index 31c6f913948..7d21e74380b 100644 --- a/services/federator/src/Federator/Interpreter.hs +++ b/services/federator/src/Federator/Interpreter.hs @@ -209,4 +209,4 @@ getFederationDomainConfigs env = do clientEnv = mkClientEnv mgr baseurl FedUp.getFederationDomainConfigs clientEnv >>= \case Right v -> pure v - Left e -> error $ show e + Left e -> error $ displayException e diff --git a/services/federator/test/unit/Test/Federator/Client.hs b/services/federator/test/unit/Test/Federator/Client.hs index fa0ce039fb2..8bc976fe6a9 100644 --- a/services/federator/test/unit/Test/Federator/Client.hs +++ b/services/federator/test/unit/Test/Federator/Client.hs @@ -221,7 +221,7 @@ testClientConnectionError = do result <- runFederatorClient env (fedClient @'Brig @"get-user-by-handle" handle) case result of Left (FederatorClientHTTP2Error (FederatorClientConnectionError _)) -> pure () - Left x -> assertFailure $ "Expected connection error, got: " <> show x + Left x -> assertFailure $ "Expected connection error, got: " <> displayException x Right _ -> assertFailure "Expected connection with the server to fail" testResponseHeaders :: IO () diff --git a/services/federator/test/unit/Test/Federator/Options.hs b/services/federator/test/unit/Test/Federator/Options.hs index 137b6d411e5..ffb63d7977e 100644 --- a/services/federator/test/unit/Test/Federator/Options.hs +++ b/services/federator/test/unit/Test/Federator/Options.hs @@ -136,7 +136,7 @@ testSettings = Left e -> assertFailure $ "expected invalid client certificate exception, got: " - <> show e + <> displayException e Right _ -> assertFailure "expected failure for non-existing client certificate, got success", testCase "failToStartWithInvalidServerCredentials" failToStartWithInvalidServerCredentials, @@ -158,7 +158,7 @@ testSettings = Left e -> assertFailure $ "expected invalid client certificate exception, got: " - <> show e + <> displayException e Right _ -> assertFailure "expected failure for invalid private key, got success" ] @@ -184,7 +184,7 @@ failToStartWithInvalidServerCredentials = do Left e -> assertFailure $ "expected invalid client certificate exception, got: " - <> show e + <> displayException e Right _ -> assertFailure "expected failure for invalid client certificate, got success" diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 610c79be947..7d3e758030c 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -354,7 +354,7 @@ getRemoteConversationsWithFailures lusr convs = do handleFailure (Left (rcids, e)) = do P.warn $ Logger.msg ("Error occurred while fetching remote conversations" :: ByteString) - . Logger.field "error" (show e) + . Logger.field "error" (displayException e) pure . Left $ failedGetConversationRemotely (sequenceA rcids) e handleFailure (Right c) = pure . Right . traverse (.convs) $ c @@ -606,8 +606,7 @@ getSelfMember lusr cnv = do pure $ Just $ conv.cnvMembers.cmSelf getLocalSelf :: - ( Member ConversationStore r - ) => + (Member ConversationStore r) => Local UserId -> ConvId -> Sem r (Maybe Public.Member) diff --git a/services/galley/src/Galley/External/LegalHoldService/Internal.hs b/services/galley/src/Galley/External/LegalHoldService/Internal.hs index 77f77b05198..eac3a0d0100 100644 --- a/services/galley/src/Galley/External/LegalHoldService/Internal.hs +++ b/services/galley/src/Galley/External/LegalHoldService/Internal.hs @@ -59,7 +59,7 @@ makeVerifiedRequestWithManager mgr verifyFingerprints fpr (HttpsUrl url) reqBuil . Bilge.secure . prependPath (uriPath url) errHandler e = do - Log.info . Log.msg $ "error making request to legalhold service: " <> show e + Log.info . Log.msg $ "error making request to legalhold service: " <> displayException e throwM (legalHoldServiceUnavailable e) prependPath :: ByteString -> Http.Request -> Http.Request prependPath pth req = req {Http.path = pth Http.path req} diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index b11611e842f..39c18a415b4 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -515,7 +515,7 @@ assertMatchChan c match = go [] match n refill buf `catchAll` \e -> case asyncExceptionFromException e of - Just x -> error $ show (x :: SomeAsyncException) + Just x -> error $ displayException (x :: SomeAsyncException) Nothing -> go (n : buf) Nothing -> do refill buf @@ -550,8 +550,7 @@ errWith wantStatus wantBody rsp = liftIO $ do assertEqual "" wantStatus (statusCode rsp) assertBool (show $ responseBody rsp) - ( maybe False wantBody (responseJsonMaybe rsp) - ) + (maybe False wantBody (responseJsonMaybe rsp)) ------------------------------------ diff --git a/services/gundeck/src/Gundeck/Push/Native.hs b/services/gundeck/src/Gundeck/Push/Native.hs index bc6b414bbb4..a7ac2069ddb 100644 --- a/services/gundeck/src/Gundeck/Push/Native.hs +++ b/services/gundeck/src/Gundeck/Push/Native.hs @@ -310,5 +310,5 @@ logError a m exn = Log.err $ field "user" (toByteString (a ^. addrUser)) ~~ field "arn" (toText (a ^. addrEndpoint)) - ~~ field "error" (show exn) + ~~ field "error" (displayException exn) ~~ msg m diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 43dba86e9ce..562bcb10730 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -125,7 +125,7 @@ logBadCannons (uri, (err, prcs)) = do ~~ Log.field "created_at" (ms $ createdAt prc) ~~ Log.field "cannon_uri" (show uri) ~~ Log.field "resource_target" (show $ resource prc) - ~~ Log.field "http_exception" (intercalate " | " . lines . show $ err) + ~~ Log.field "http_exception" (intercalate " | " . lines . displayException $ err) ~~ Log.msg (val "WebSocket presence unreachable: ") logPrcsGone :: (Log.MonadLogger m) => Presence -> m () @@ -327,7 +327,7 @@ push notif (toList -> tgts) originUser originConn conns = do <$> runWithDefaultRedis (Presence.listAll (view targetUser <$> tgts)) noPresences exn = do Log.err $ - Log.field "error" (show exn) + Log.field "error" (displayException exn) ~~ Log.msg (val "Failed to get presences.") pure [] filterByClient = map $ \(tgt, ps) -> diff --git a/services/gundeck/src/Gundeck/Redis.hs b/services/gundeck/src/Gundeck/Redis.hs index 5a8ba319caa..17e1f2e3171 100644 --- a/services/gundeck/src/Gundeck/Redis.hs +++ b/services/gundeck/src/Gundeck/Redis.hs @@ -85,8 +85,8 @@ connectRobust l retryStrategy connectLowLevel = do const $ Catch.Handler (\(e :: IOException) -> logEx (Log.err l) e "network error when connecting to Redis" >> pure True) ] . const -- ignore RetryStatus - logEx :: (Show e) => ((Msg -> Msg) -> IO ()) -> e -> ByteString -> IO () - logEx lLevel e description = lLevel $ Log.msg (Log.val description) . Log.field "error" (show e) + logEx :: (Exception e) => ((Msg -> Msg) -> IO ()) -> e -> ByteString -> IO () + logEx lLevel e description = lLevel $ Log.msg (Log.val description) . Log.field "error" (displayException e) -- | Run a 'Redis' action through a 'RobustConnection'. -- @@ -107,7 +107,7 @@ runRobust mvar action = retry $ do . const -- ignore RetryStatus logAndHandle (Handler handler) _ = Handler $ \e -> do - LogClass.err $ Log.msg (Log.val "Redis connection failed") . Log.field "error" (show e) + LogClass.err $ Log.msg (Log.val "Redis connection failed") . Log.field "error" (displayException e) handler e data PingException = PingException Reply deriving (Show) diff --git a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs index 89595b9d51b..44d7953c93d 100644 --- a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs +++ b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs @@ -308,5 +308,5 @@ safeForever :: safeForever action = forever $ action `catchAny` \exc -> do - LC.err $ "error" LC..= show exc LC.~~ LC.msg (LC.val "watchThreadBudgetState: crashed; retrying") + LC.err $ "error" LC..= displayException exc LC.~~ LC.msg (LC.val "watchThreadBudgetState: crashed; retrying") threadDelay 60000000 -- pause to keep worst-case noise in logs manageable diff --git a/services/proxy/src/Proxy/API/Public.hs b/services/proxy/src/Proxy/API/Public.hs index c374a47f77e..5b7e01a4d77 100644 --- a/services/proxy/src/Proxy/API/Public.hs +++ b/services/proxy/src/Proxy/API/Public.hs @@ -134,7 +134,7 @@ proxy qparam keyname reroute path phost rq k = do onUpstreamError :: (Proxy () -> IO a) -> SomeException -> p -> (Response -> IO b) -> IO b onUpstreamError runInIO x _ next = do - void . runInIO $ Logger.warn (msg (val "gateway error") ~~ field "error" (show x)) + void . runInIO $ Logger.warn (msg (val "gateway error") ~~ field "error" (displayException x)) next (errorRs error502) waiProxyResponse :: Env -> Request -> ProxyDest -> WaiProxyResponse diff --git a/services/spar/src/Spar/Sem/SAML2/Library.hs b/services/spar/src/Spar/Sem/SAML2/Library.hs index 78e8c08c3c9..7ca06008ffb 100644 --- a/services/spar/src/Spar/Sem/SAML2/Library.hs +++ b/services/spar/src/Spar/Sem/SAML2/Library.hs @@ -59,7 +59,7 @@ wrapMonadClientSPImpl action = . SAML.CustomError . SparCassandraError . LText.pack - . show @SomeException + . displayException @SomeException ) instance (Member (Final IO) r) => Catch.MonadThrow (SPImpl r) where diff --git a/services/spar/src/Spar/Sem/Utils.hs b/services/spar/src/Spar/Sem/Utils.hs index 381b2881717..0cac0ec9db4 100644 --- a/services/spar/src/Spar/Sem/Utils.hs +++ b/services/spar/src/Spar/Sem/Utils.hs @@ -64,7 +64,7 @@ interpretClientToIO ctx = interpret $ \case . SAML.CustomError . SparCassandraError . LText.pack - . show @SomeException + . displayException @SomeException pure $ action' `Catch.catch` \e -> handler' $ e <$ st ttlErrorToSparError :: (Member (Error SparError) r) => Sem (Error TTLError ': r) a -> Sem r a diff --git a/tools/entreprise-provisioning/default.nix b/tools/entreprise-provisioning/default.nix index b2e1cd807af..ac8d0863103 100644 --- a/tools/entreprise-provisioning/default.nix +++ b/tools/entreprise-provisioning/default.nix @@ -23,6 +23,7 @@ , types-common , uuid , vector +, wai-utilities , wire-api }: mkDerivation { @@ -46,6 +47,7 @@ mkDerivation { types-common uuid vector + wai-utilities wire-api ]; testHaskellDepends = [ diff --git a/tools/entreprise-provisioning/entreprise-provisioning.cabal b/tools/entreprise-provisioning/entreprise-provisioning.cabal index fa89078a1b9..61f914ce513 100644 --- a/tools/entreprise-provisioning/entreprise-provisioning.cabal +++ b/tools/entreprise-provisioning/entreprise-provisioning.cabal @@ -37,6 +37,7 @@ executable entreprise-provisioning , types-common , uuid , vector + , wai-utilities , wire-api ghc-options: diff --git a/tools/entreprise-provisioning/src/API.hs b/tools/entreprise-provisioning/src/API.hs index 90bca4861bf..f17908a4148 100644 --- a/tools/entreprise-provisioning/src/API.hs +++ b/tools/entreprise-provisioning/src/API.hs @@ -34,6 +34,7 @@ import Data.Vector qualified as V import Imports import Network.HTTP.Client import Network.HTTP.Types.Status +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import Types import Wire.API.Conversation import Wire.API.Conversation.Role (roleNameWireAdmin) @@ -85,7 +86,7 @@ createChannel manager (ApiUrl apiUrl) (Token token) userId teamId channelName = result <- try $ httpLbs request manager case result of Left (e :: HttpException) -> - pure $ Left $ ErrorDetail 0 (object ["error" .= show e]) + pure $ Left $ ErrorDetail 0 (object ["error" .= displayExceptionNoBacktrace e]) Right resp -> let respStatus = statusCode (responseStatus resp) in case respStatus of @@ -130,7 +131,7 @@ associateChannelsToGroup manager (ApiUrl apiUrl) (Token token) userId groupId co result <- try $ httpLbs request manager case result of Left (e :: HttpException) -> - pure $ Left $ ErrorDetail 0 (object ["error" .= show e]) + pure $ Left $ ErrorDetail 0 (object ["error" .= displayExceptionNoBacktrace e]) Right resp -> case statusCode (responseStatus resp) of 200 -> pure $ Right () diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index db21771a9a6..98e262a6531 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -52,6 +52,7 @@ import Imports hiding (head) import Network.HTTP.Types import Network.Wai import Network.Wai.Utilities as Wai +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import Network.Wai.Utilities.Server import Network.Wai.Utilities.Server qualified as Server import Servant (NoContent (NoContent), ServerT, (:<|>) (..)) @@ -460,10 +461,10 @@ getUserData uid mMaxConvs mMaxNotifs = do -- galeb consent <- (Intra.getUserConsentValue uid <&> toJSON @ConsentValue) - `catchE` (pure . String . T.pack . show) + `catchE` (pure . String . T.pack . displayExceptionNoBacktrace) consentLog <- (Intra.getUserConsentLog uid <&> toJSON @ConsentLog) - `catchE` (pure . String . T.pack . show) + `catchE` (pure . String . T.pack . displayExceptionNoBacktrace) let em = userEmail account marketo <- do let noEmail = MarketoResult $ KeyMap.singleton "results" emptyArray @@ -471,7 +472,7 @@ getUserData uid mMaxConvs mMaxNotifs = do (pure $ toJSON noEmail) ( \e -> (Intra.getMarketoResult e <&> toJSON) - `catchE` (pure . String . T.pack . show) + `catchE` (pure . String . T.pack . displayExceptionNoBacktrace) ) em pure . UserMetaInfo . KeyMap.fromList $ diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index bae73c12ca9..6daee4beffe 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -655,7 +655,7 @@ catchRpcErrors action = ExceptT $ catch (Right <$> action) catchRPCException catchRPCException :: RPCException -> App (Either Error a) catchRPCException rpcE = do Log.err $ rpcExceptionMsg rpcE - pure . Left $ mkError status500 "io-error" (pack $ show rpcE) + pure . Left $ mkError status500 "io-error" (pack $ displayExceptionNoBacktrace rpcE) getTeamData :: TeamId -> Handler TeamData getTeamData tid = do From dd24ad5322884ac7d25b211c05c22e098f3e67f9 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Mon, 5 Jan 2026 11:59:59 +0100 Subject: [PATCH 40/60] Fix HLS setup: Remove protoc cabal override (#4928) Allowing any newer version of protoc led to issues running the Haskell Language Server (HLS). This override in `cabal.project` has now been removed. A newer version than the one available via Nix shouldn't be required. --- cabal.project | 12 ------------ changelog.d/5-internal/remove-protoc-cabal-override | 1 + 2 files changed, 1 insertion(+), 12 deletions(-) create mode 100644 changelog.d/5-internal/remove-protoc-cabal-override diff --git a/cabal.project b/cabal.project index 80f34bf09e3..8ef7a564743 100644 --- a/cabal.project +++ b/cabal.project @@ -68,15 +68,3 @@ benchmarks: True program-options ghc-options: -Werror - --- NOTE: --- - these packages are not provided by nix, reason being, that --- there is a bug in the nixpkgs haskell compatibility which --- makes it such that they cannot be installed by the nixpkgs code --- - these packages have bounds that are justified with their current --- dependency set, however, we have updated their dependencies, such --- that they work with newer base and ghc (api) versions -allow-newer: - , proto-lens-protoc:base - , proto-lens-protoc:ghc - , proto-lens-setup:Cabal diff --git a/changelog.d/5-internal/remove-protoc-cabal-override b/changelog.d/5-internal/remove-protoc-cabal-override new file mode 100644 index 00000000000..9db8396da96 --- /dev/null +++ b/changelog.d/5-internal/remove-protoc-cabal-override @@ -0,0 +1 @@ +Allowing any newer version of protoc led to issues running the Haskell Language Server (HLS). This override in `cabal.project` has now been removed. A newer version than the one available via Nix shouldn't be required. From b9e76c17a686c1f6a85fb51926f1f7d842c4527f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20L=C3=A4ll?= Date: Mon, 5 Jan 2026 15:35:51 +0200 Subject: [PATCH 41/60] Fix ToSchema instance for SearchResult (#4921) Co-authored-by: Akshay Mankar --- .../WPB-22297-Fix-ToSchema-instance-for-SearchResult | 1 + libs/wire-api/src/Wire/API/User/Search.hs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-22297-Fix-ToSchema-instance-for-SearchResult diff --git a/changelog.d/3-bug-fixes/WPB-22297-Fix-ToSchema-instance-for-SearchResult b/changelog.d/3-bug-fixes/WPB-22297-Fix-ToSchema-instance-for-SearchResult new file mode 100644 index 00000000000..266490f0a2c --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-22297-Fix-ToSchema-instance-for-SearchResult @@ -0,0 +1 @@ +Make Swagger schema instances for `GET /search/results` and `GET /teams/{tid}/search` distinct \ No newline at end of file diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index 3eb2cda6d58..bddf084994a 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -54,6 +54,7 @@ import Data.Schema import Data.Text qualified as T import Data.Text.Ascii (AsciiBase64Url, toText, validateBase64Url) import Data.Text.Encoding qualified as TE +import Data.Typeable (typeRep) import Imports import Servant.API (FromHttpApiData, ToHttpApiData (..)) import Web.Internal.HttpApiData (parseQueryParam) @@ -110,9 +111,9 @@ instance Traversable SearchResult where newResults <- traverse f (searchResults r) pure $ r {searchResults = newResults} -instance (ToSchema a) => ToSchema (SearchResult a) where +instance (ToSchema a, Typeable a) => ToSchema (SearchResult a) where schema = - object "SearchResult" $ + object ("SearchResult_" <> T.pack (show $ typeRep $ Proxy @a)) $ SearchResult <$> searchFound .= fieldWithDocModifier "found" (S.description ?~ "Total number of hits") schema <*> searchReturned .= fieldWithDocModifier "returned" (S.description ?~ "Total number of hits returned") schema From 8f52f26b08fda4e504b3478f144005664c95447f Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 5 Jan 2026 16:36:44 +0100 Subject: [PATCH 42/60] WPB-22515: upgrade ormolu (#4923) --- changelog.d/5-internal/WPB-22515 | 1 + integration/test/API/BrigInternal.hs | 2 +- integration/test/API/Galley.hs | 3 +- integration/test/MLS/Util.hs | 2 +- integration/test/Test/Conversation.hs | 6 ++-- integration/test/Test/DomainVerification.hs | 6 ++-- integration/test/Test/UserGroup.hs | 18 +++++------ integration/test/Testlib/ModService.hs | 8 ++--- .../polysemy-wire-zoo/src/Polysemy/TinyLog.hs | 3 +- .../src/Wire/Sem/Now/Spec.hs | 3 +- libs/saml2-web-sso/src/SAML2/WebSSO/Cookie.hs | 2 +- libs/saml2-web-sso/src/SAML2/WebSSO/Types.hs | 8 ++--- libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs | 2 +- .../src/Wire/API/Federation/Endpoint.hs | 6 ++-- libs/wire-api/src/Wire/API/Conversation.hs | 3 +- .../Wire/API/Routes/Internal/Enterprise.hs | 3 +- .../src/Wire/API/Routes/Internal/Galley.hs | 3 +- .../src/Wire/API/Routes/Public/Brig.hs | 9 ++---- .../Routes/Public/Brig/DomainVerification.hs | 6 ++-- .../src/Wire/API/Routes/Public/Spar.hs | 3 +- libs/wire-api/src/Wire/API/Routes/Version.hs | 3 +- libs/wire-api/src/Wire/API/Team/Member.hs | 12 ++++---- libs/wire-api/src/Wire/API/User/Activation.hs | 3 +- libs/wire-api/src/Wire/API/UserEvent.hs | 6 ++-- .../Wire/API/Golden/Generated/NewUser_user.hs | 3 +- .../API/Golden/Generated/RTCIceServer_user.hs | 3 +- .../API/Golden/Generated/SimpleMember_user.hs | 6 ++-- .../Wire/API/Golden/Manual/LoginId_user.hs | 3 +- .../Wire/API/Golden/Manual/SubConversation.hs | 6 ++-- .../Test/Wire/API/Golden/Manual/UserEvent.hs | 6 ++-- .../Wire/BackgroundJobsPublisher/RabbitMQ.hs | 3 +- .../src/Wire/ConversationStore/Cassandra.hs | 3 +- .../src/Wire/ConversationStore/Postgres.hs | 4 +-- .../Cassandra.hs | 3 +- .../EnterpriseLoginSubsystem/Interpreter.hs | 3 +- .../Wire/FederationConfigStore/Cassandra.hs | 3 +- .../Wire/IndexedUserStore/ElasticSearch.hs | 3 +- .../src/Wire/ScimSubsystem/Interpreter.hs | 2 +- .../TeamCollaboratorsSubsystem/Interpreter.hs | 9 ++---- .../src/Wire/TeamSubsystem/Util.hs | 2 +- .../src/Wire/UserGroupStore/Postgres.hs | 3 +- .../Wire/UserGroupSubsystem/Interpreter.hs | 3 +- .../MockInterpreters/ActivationCodeStore.hs | 3 +- .../Wire/UserSubsystem/InterpreterSpec.hs | 2 +- nix/wire-server.nix | 4 +-- services/brig/src/Brig/API/Internal.hs | 30 +++++++------------ services/brig/src/Brig/API/Public.hs | 14 ++++----- services/brig/src/Brig/API/User.hs | 6 ++-- services/brig/src/Brig/Provider/API.hs | 6 ++-- .../brig/test/integration/API/User/Auth.hs | 3 +- .../test/integration/Federation/End2end.hs | 6 ++-- .../galley/src/Galley/API/Action/Notify.hs | 3 +- services/galley/src/Galley/API/Federation.hs | 3 +- services/galley/src/Galley/API/Internal.hs | 3 +- .../galley/src/Galley/API/MLS/CheckClients.hs | 4 +-- .../galley/src/Galley/API/MLS/GroupInfo.hs | 3 +- services/galley/src/Galley/API/MLS/Message.hs | 3 +- services/galley/src/Galley/App.hs | 3 +- services/galley/test/integration/API.hs | 3 +- .../galley/test/integration/API/MLS/Mocks.hs | 3 +- .../galley/test/integration/API/MLS/Util.hs | 3 +- services/galley/test/integration/API/Teams.hs | 3 +- services/gundeck/test/unit/MockGundeck.hs | 4 +-- services/spar/src/Spar/API.hs | 2 +- .../test-integration/Test/Spar/APISpec.hs | 16 ++++------ tools/stern/src/Stern/API.hs | 2 +- 66 files changed, 123 insertions(+), 198 deletions(-) create mode 100644 changelog.d/5-internal/WPB-22515 diff --git a/changelog.d/5-internal/WPB-22515 b/changelog.d/5-internal/WPB-22515 new file mode 100644 index 00000000000..c2acd72b4b7 --- /dev/null +++ b/changelog.d/5-internal/WPB-22515 @@ -0,0 +1 @@ +Upgrade ormolu to match GHC 9.10. diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index d23d793f2e7..03775fc5144 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -66,7 +66,7 @@ createUser domain cu = do [ "name" .= "integration test team", "icon" .= "default" ] - | cu.team + | cu.team ] ) diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 96fb09923b8..391b162b776 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -595,8 +595,7 @@ updateRole caller target role qcnv = do caller Galley Versioned - ( joinHttpPath ["conversations", cnvDomain, cnvId, "members", tarDomain, tarId] - ) + (joinHttpPath ["conversations", cnvDomain, cnvId, "members", tarDomain, tarId]) submit "PUT" (req & addJSONObject ["conversation_role" .= roleReq]) updateReceiptMode :: diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 827b317b158..54971aa8e13 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -181,7 +181,7 @@ initMLSClient opts cid = do let keys = object [ csSignatureScheme ciphersuite .= T.decodeUtf8 (Base64.encode pkey) - | (ciphersuite, pkey) <- suitePKeys + | (ciphersuite, pkey) <- suitePKeys ] bindResponse ( updateClient diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 131260d5b7d..dfd5a9d96c9 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -104,9 +104,9 @@ testDynamicBackendsFullyConnectedWhenAllowDynamic = do -- between backends when the federation strategy is 'allowDynamic'. sequence_ [ BrigI.createFedConn x (BrigI.FedConn y "full_search" Nothing) - | x <- [domainA, domainB, domainC], - y <- [domainA, domainB, domainC], - x /= y + | x <- [domainA, domainB, domainC], + y <- [domainA, domainB, domainC], + x /= y ] uidA <- randomUser domainA def {BrigI.team = True} uidB <- randomUser domainB def {BrigI.team = True} diff --git a/integration/test/Test/DomainVerification.hs b/integration/test/Test/DomainVerification.hs index bec68fb8546..f80df2547a2 100644 --- a/integration/test/Test/DomainVerification.hs +++ b/integration/test/Test/DomainVerification.hs @@ -273,8 +273,7 @@ testUpdateTeamInvite = forM_ [ExplicitVersion 8, Versioned] \version -> do -- admin should not be able to set team-invite if the team hasn't been authorized bindResponse - ( updateTeamInvite owner domain (object ["team_invite" .= "team", "team" .= tid]) - ) + (updateTeamInvite owner domain (object ["team_invite" .= "team", "team" .= tid])) $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "operation-forbidden-for-domain-registration-state" @@ -283,8 +282,7 @@ testUpdateTeamInvite = forM_ [ExplicitVersion 8, Versioned] \version -> do -- non-admin should not be able to set team-invite bindResponse - ( updateTeamInvite mem domain (object ["team_invite" .= "team", "team" .= tid]) - ) + (updateTeamInvite mem domain (object ["team_invite" .= "team", "team" .= tid])) $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "operation-forbidden-for-domain-registration-state" diff --git a/integration/test/Test/UserGroup.hs b/integration/test/Test/UserGroup.hs index ac0101c68b3..c03c1389e22 100644 --- a/integration/test/Test/UserGroup.hs +++ b/integration/test/Test/UserGroup.hs @@ -359,15 +359,15 @@ testUserGroupGetGroupsAllInputs = do includeMemberCount = includeMemberCount', includeChannels = includeChannels' } - | q' <- qs, - sortBy' <- sortByKeysList, - sortOrder' <- sortOrders, - pSize' <- pSizes, - lastName' <- lastNames, - lastCreatedAt' <- lastCreatedAts, - lastId' <- lastIds, - includeMemberCount' <- [False, True], - includeChannels' <- [False, True] + | q' <- qs, + sortBy' <- sortByKeysList, + sortOrder' <- sortOrders, + pSize' <- pSizes, + lastName' <- lastNames, + lastCreatedAt' <- lastCreatedAts, + lastId' <- lastIds, + includeMemberCount' <- [False, True], + includeChannels' <- [False, True] ] where qs = [Nothing, Just "A"] diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index e111bcb76b2..3939c17164f 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -304,10 +304,10 @@ updateServiceMapInConfig resource forSrv config = ) config [ (srv, berInternalServicePorts resource srv :: Int) - | srv <- allServices, - -- if a service is not enabled, do not set its endpoint configuration, - -- unless we are starting the service itself - berEnableService resource srv || srv == forSrv + | srv <- allServices, + -- if a service is not enabled, do not set its endpoint configuration, + -- unless we are starting the service itself + berEnableService resource srv || srv == forSrv ] startBackend :: diff --git a/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs b/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs index 4a72cb8f8c9..b40f904e7f9 100644 --- a/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs +++ b/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs @@ -53,8 +53,7 @@ logErrors showError msg action = Polysemy.Error.catch action $ \e -> do logAndIgnoreErrors :: forall e r. - ( Member TinyLog r - ) => + (Member TinyLog r) => (e -> Text) -> Text -> Sem (Error e ': r) () -> diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Spec.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Spec.hs index 84b789646ba..9e7b65ee139 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Spec.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Spec.hs @@ -65,5 +65,4 @@ prop_nowNow = pure $ simpleLaw (liftA2 (<=) E.get E.get) - ( pure True - ) + (pure True) diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/Cookie.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/Cookie.hs index 2afd63ddf29..d11cdc59b19 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/Cookie.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/Cookie.hs @@ -52,7 +52,7 @@ headerValueToCookie txt = do let cookie = parseSetCookie $ cs txt case ["missing cookie name" | setCookieName cookie == ""] <> [ cs $ "wrong cookie name: got " <> setCookieName cookie <> ", expected " <> cookieName (Proxy @name) - | setCookieName cookie /= cookieName (Proxy @name) + | setCookieName cookie /= cookieName (Proxy @name) ] <> ["missing cookie value" | setCookieValue cookie == ""] of errs@(_ : _) -> throwError $ ST.intercalate ", " errs diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/Types.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/Types.hs index e5fd8b53165..21c57322871 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/Types.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/Types.hs @@ -441,19 +441,19 @@ mkNameID nid@(UNameIDEntity uri) m1 m2 m3 = do mapM_ throwError $ [ "mkNameID: nameIDNameQ, nameIDSPNameQ, nameIDSPProvidedID MUST be omitted for entity NameIDs." <> show [m1, m2, m3] - | all isJust [m1, m2, m3] + | all isJust [m1, m2, m3] ] <> [ "mkNameID: entity URI too long: " <> show uritxt - | uritxt <- [renderURI uri], - ST.length uritxt > 1024 + | uritxt <- [renderURI uri], + ST.length uritxt > 1024 ] pure $ NameID nid Nothing Nothing Nothing mkNameID nid@(UNameIDPersistent txt) m1 m2 m3 = do mapM_ throwError $ [ "mkNameID: persistent text too long: " <> show (nid, ST.length txt) - | ST.length txt > 1024 + | ST.length txt > 1024 ] pure $ NameID nid m1 m2 m3 mkNameID nid m1 m2 m3 = do diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs index e3b77b281f3..3a17412898e 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs @@ -517,7 +517,7 @@ exportConditions conds = HX.conditions = [HX.OneTimeUse | conds ^. condOneTimeUse] <> [ HX.AudienceRestriction (HX.Audience . exportURI <$> hsrs) - | hsrs <- conds ^. condAudienceRestriction + | hsrs <- conds ^. condAudienceRestriction ] } diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs index f93c1ed6ae6..befd35a88aa 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs @@ -63,8 +63,7 @@ type UnnamedFedEndpointWithMods (mods :: [Type]) path input output = type FedEndpointWithMods (mods :: [Type]) name input output = Named name - ( UnnamedFedEndpointWithMods mods (FedPath name) input output - ) + (UnnamedFedEndpointWithMods mods (FedPath name) input output) type FedEndpoint name input output = FedEndpointWithMods '[] name input output @@ -156,8 +155,7 @@ data StreamPostWithRemoteIp framing (ct :: Type) a -- Server-side simply delegates to the standard 'StreamPost' implementation. instance - ( HasServer (StreamPost framing ct a) context - ) => + (HasServer (StreamPost framing ct a) context) => HasServer (StreamPostWithRemoteIp framing ct a) context where type ServerT (StreamPostWithRemoteIp framing ct a) m = ServerT (StreamPost framing ct a) m diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 839d31fe012..d10ad9c6f0d 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -954,8 +954,7 @@ newConvSchema v sch = <*> newConvCells .= (fromMaybe False <$> optField "cells" schema) <*> newConvChannelAddPermission .= maybe_ - ( optFieldWithDocModifier "add_permission" (description ?~ "Channel add permission") schema - ) + (optFieldWithDocModifier "add_permission" (description ?~ "Channel add permission") schema) <*> newConvSkipCreator .= ( fromMaybe False <$> optFieldWithDocModifier diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Enterprise.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Enterprise.hs index ca5e5702970..b8e3e0509a2 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Enterprise.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Enterprise.hs @@ -29,8 +29,7 @@ type InternalAPI = "i" :> InternalAPIBase type InternalAPIBase = Named "status" - ( "status" :> MultiVerb 'GET '[JSON] '[RespondEmpty 200 "OK"] () - ) + ("status" :> MultiVerb 'GET '[JSON] '[RespondEmpty 200 "OK"] ()) :<|> Named "create-verification-token" ( "create-verification-token" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index 1293fcd7aa7..ea44ce08034 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -119,8 +119,7 @@ type InternalAPI = "i" :> InternalAPIBase type InternalAPIBase = Named "status" - ( "status" :> MultiVerb 'GET '[JSON] '[RespondEmpty 200 "OK"] () - ) + ("status" :> MultiVerb 'GET '[JSON] '[RespondEmpty 200 "OK"] ()) -- This endpoint can lead to the following events being sent: -- - MemberLeave event to members for all conversations the user was in :<|> Named diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index e947cfa2077..313a12fe372 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1040,8 +1040,7 @@ type UserClientAPI = :> MultiVerb1 'GET '[JSON] - ( VersionedRespond 'V6 200 "List of clients" [Client] - ) + (VersionedRespond 'V6 200 "List of clients" [Client]) ) :<|> Named "list-clients@v7" @@ -1053,8 +1052,7 @@ type UserClientAPI = :> MultiVerb1 'GET '[JSON] - ( VersionedRespond 'V7 200 "List of clients" [Client] - ) + (VersionedRespond 'V7 200 "List of clients" [Client]) ) :<|> Named "list-clients" @@ -1065,8 +1063,7 @@ type UserClientAPI = :> MultiVerb1 'GET '[JSON] - ( Respond 200 "List of clients" [Client] - ) + (Respond 200 "List of clients" [Client]) ) :<|> Named "get-client@v6" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs index c054e553061..e7447c46a0a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs @@ -299,8 +299,7 @@ instance ToSchema DomainRedirectResponseV9 where DomainRedirectResponse <$> (\r -> True <$ guard r.propagateUserExists) .= maybe_ - ( fromMaybe False <$> optField "due_to_existing_account" schema - ) + (fromMaybe False <$> optField "due_to_existing_account" schema) <*> (.redirect) .= domainRedirectSchemaV9 type DomainRedirectResponseV10 = DomainRedirectResponse V10 @@ -319,8 +318,7 @@ instance ToSchema DomainRedirectResponseV10 where DomainRedirectResponse <$> (\r -> True <$ guard r.propagateUserExists) .= maybe_ - ( fromMaybe False <$> optField "due_to_existing_account" schema - ) + (fromMaybe False <$> optField "due_to_existing_account" schema) <*> (.redirect) .= domainRedirectSchema V10 type DomainVerificationChallengeAPI = diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index 6a40c1330b7..642b9dc5225 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -170,8 +170,7 @@ type IdpDelete = type SsoSettingsGet = Named "sso-settings" - ( Get '[JSON] SsoSettings - ) + (Get '[JSON] SsoSettings) sparSPIssuer :: (Functor m, SAML.HasConfig m) => Maybe TeamId -> Maybe Domain -> m (Maybe SAML.Issuer) sparSPIssuer mbtid = (SAML.Issuer <$$>) . sparResponseURI mbtid diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 5c422255089..b251e039bf6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -299,8 +299,7 @@ instance ToSchema VersionExp where <> tag _VersionExpDevelopment ( unnamed - ( enum @Text "VersionExpDevelopment" (element "development" ()) - ) + (enum @Text "VersionExpDevelopment" (element "development" ())) ) deriving via Schema VersionExp instance (FromJSON VersionExp) diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index d11f69e14d0..2d75ce2e04d 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -521,12 +521,12 @@ permissionsRole (Permissions p p') = permsRole perms = listToMaybe [ role - | role <- [minBound ..], - -- if a there is a role that is strictly less permissive than the perms set that - -- we encounter, we downgrade. this shouldn't happen in real life, but it has - -- happened to very old users on a staging environment, where a user (probably) - -- was create before the current publicly visible permissions had been stabilized. - rolePerms role `Set.isSubsetOf` perms + | role <- [minBound ..], + -- if a there is a role that is strictly less permissive than the perms set that + -- we encounter, we downgrade. this shouldn't happen in real life, but it has + -- happened to very old users on a staging environment, where a user (probably) + -- was create before the current publicly visible permissions had been stabilized. + rolePerms role `Set.isSubsetOf` perms ] -- | Internal function for 'rolePermissions'. (It works iff the two sets in 'Permissions' are diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index 84c993870b4..e6ee35fd35c 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -205,8 +205,7 @@ instance ToSchema SendActivationCode where .= maybe_ ( optFieldWithDocModifier "locale" - ( description ?~ "Locale to use for the activation code template." - ) + (description ?~ "Locale to use for the activation code template.") schema ) where diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index a2bf6e4a39c..89e6a600af3 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -319,13 +319,11 @@ eventObjectSchema = EventTypeUserLegalholdEnabled -> tag _UserEvent - ( tag _UserLegalHoldEnabled (field "id" schema) - ) + (tag _UserLegalHoldEnabled (field "id" schema)) EventTypeUserLegalholdDisabled -> tag _UserEvent - ( tag _UserLegalHoldDisabled (field "id" schema) - ) + (tag _UserLegalHoldDisabled (field "id" schema)) EventTypeUserLegalholdRequested -> tag _UserEvent diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs index 77084a3e648..a910f962d96 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs @@ -160,8 +160,7 @@ testObject_NewUser_user_8 = newUserIdentity = Just ( EmailIdentity - ( unsafeEmailAddress "some" "example" - ) + (unsafeEmailAddress "some" "example") ), newUserPassword = Just (plainTextPassword8Unsafe "12345678") } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCIceServer_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCIceServer_user.hs index 27d2122fe79..d6a7f3dabce 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCIceServer_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCIceServer_user.hs @@ -62,8 +62,7 @@ testObject_RTCIceServer_user_1 = testObject_RTCIceServer_user_2 :: RTCIceServer testObject_RTCIceServer_user_2 = rtcIceServer - ( NonEmpty.singleton (turnURI SchemeTurn (TurnHostIp (IpAddr (read "108.37.81.160"))) (read "0") (Just TransportTCP)) - ) + (NonEmpty.singleton (turnURI SchemeTurn (TurnHostIp (IpAddr (read "108.37.81.160"))) (read "0") (Just TransportTCP))) ( turnUsername (secondsToNominalDiffTime 3.000000000000) "a8kdffu4" & tuVersion .~ 5 & tuKeyindex .~ 24 diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SimpleMember_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SimpleMember_user.hs index ab173f4d92e..4a5b21046be 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SimpleMember_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SimpleMember_user.hs @@ -31,8 +31,7 @@ testObject_SimpleMember_user_1 = { smQualifiedId = Qualified (Id (fromJust (UUID.fromString "0000003a-0000-0042-0000-007500000037"))) (Domain "faraway.example.com"), smConvRoleName = fromJust - ( parseRoleName "wire_member" - ) + (parseRoleName "wire_member") } testObject_SimpleMember_user_2 :: SimpleMember @@ -41,6 +40,5 @@ testObject_SimpleMember_user_2 = { smQualifiedId = Qualified (Id (fromJust (UUID.fromString "0000003a-0000-0042-0000-007500000037"))) (Domain "faraway.example.com"), smConvRoleName = fromJust - ( parseRoleName "wire_admin" - ) + (parseRoleName "wire_admin") } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs index c739d7d034b..ead2775ee40 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs @@ -30,8 +30,7 @@ testObject_LoginId_user_1 = testObject_LoginId_user_2 :: LoginId testObject_LoginId_user_2 = LoginByEmail - ( unsafeEmailAddress "some" "example" - ) + (unsafeEmailAddress "some" "example") testObject_LoginId_user_3 :: LoginId testObject_LoginId_user_3 = LoginByHandle (fromJust (parseHandle "7a8gg3v98")) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs index eda0dcb51aa..8ae7df9b026 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs @@ -46,8 +46,7 @@ domain = Domain "golden.example.com" convId :: Qualified ConvId convId = Qualified - ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")) - ) + (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) domain testObject_PublicSubConversation_1 :: PublicSubConversation @@ -82,7 +81,6 @@ testObject_PublicSubConversation_2 = user :: Qualified UserId user = Qualified - ( Id (fromJust (UUID.fromString "00000000-0000-0007-0000-000a00000002")) - ) + (Id (fromJust (UUID.fromString "00000000-0000-0007-0000-000a00000002"))) domain cid = ClientId 0xdeadbeef diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs index e24036faaa1..6a6ea459187 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs @@ -160,14 +160,12 @@ testObject_UserEvent_12 = testObject_UserEvent_13 :: Event testObject_UserEvent_13 = PropertyEvent - ( PropertySet (PropertyKey "a") (toJSON (39 :: Int)) - ) + (PropertySet (PropertyKey "a") (toJSON (39 :: Int))) testObject_UserEvent_14 :: Event testObject_UserEvent_14 = PropertyEvent - ( PropertyDeleted (PropertyKey "a") - ) + (PropertyDeleted (PropertyKey "a")) testObject_UserEvent_15 :: Event testObject_UserEvent_15 = PropertyEvent PropertiesCleared diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs index 78b3b9aa8b1..fecd5ca6bd2 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs @@ -38,8 +38,7 @@ interpretBackgroundJobsPublisherRabbitMQ requestId channelMVar = publishJob requestId channel jobId jobPayload publishJob :: - ( Member (Embed IO) r - ) => + (Member (Embed IO) r) => RequestId -> Q.Channel -> JobId -> diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Cassandra.hs index cab006f3464..66f3943b90e 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Cassandra.hs @@ -260,8 +260,7 @@ localConversations :: Sem r [StoredConversation] localConversations client = collectAndLog - <=< ( runEmbedded (runClient client) . embed . UnliftIO.pooledMapConcurrentlyN 8 localConversation' - ) + <=< (runEmbedded (runClient client) . embed . UnliftIO.pooledMapConcurrentlyN 8 localConversation') where collectAndLog cs = case partitionEithers cs of (errs, convs) -> traverse_ (warn . Log.msg) errs $> convs diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs index 7d8b26ce82e..ff1c89490cf 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs @@ -1276,8 +1276,8 @@ searchConversationsImpl req = (sortOrderOperator req.sortOrder) -- the pagination cursor must match the ORDER BY. Therefore the comparison is case-insensitive. (mkClause "lower(name)" (Text.toLower lastName) <> mkClause "id" lastId) - | lastName <- toList req.lastName, - lastId <- toList req.lastId + | lastName <- toList req.lastName, + lastId <- toList req.lastId ] <> toList (like "name" <$> req.searchString) <> discoverableClause diff --git a/libs/wire-subsystems/src/Wire/DomainVerificationChallengeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/DomainVerificationChallengeStore/Cassandra.hs index 03ba7a61013..44ed929a560 100644 --- a/libs/wire-subsystems/src/Wire/DomainVerificationChallengeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/DomainVerificationChallengeStore/Cassandra.hs @@ -34,8 +34,7 @@ import Wire.DomainVerificationChallengeStore interpretDomainVerificationChallengeStoreToCassandra :: forall r. - ( Member (Embed IO) r - ) => + (Member (Embed IO) r) => ClientState -> Timeout -> InterpreterFor DomainVerificationChallengeStore r diff --git a/libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem/Interpreter.hs index 1f203564ed9..54539dcd495 100644 --- a/libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem/Interpreter.hs @@ -559,8 +559,7 @@ sendAuditMail url subject mBefore mAfter = do url <> " called;\nOld value:\n" <> fromLazyText - ( LT.decodeUtf8 (encodeDomainRegistrationPretty mBefore) - ) + (LT.decodeUtf8 (encodeDomainRegistrationPretty mBefore)) <> "\nNew value:\n" <> fromLazyText ( LT.decodeUtf8 diff --git a/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs index 2038fc697ed..fcae3c1261a 100644 --- a/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs @@ -47,8 +47,7 @@ import Wire.FederationConfigStore -- In the future the config file will be removed and the database will be the only source of truth. interpretFederationDomainConfig :: forall r a. - ( Member (Embed IO) r - ) => + (Member (Embed IO) r) => ClientState -> Maybe FederationStrategy -> Map Domain FederationDomainConfig -> diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs index 05241eadcdc..320199e48d1 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs @@ -76,8 +76,7 @@ interpretIndexedUserStoreES cfg = GetTeamSize tid -> getTeamSizeImpl cfg tid getTeamSizeImpl :: - ( Member (Embed IO) r - ) => + (Member (Embed IO) r) => IndexedUserStoreConfig -> TeamId -> Sem r TeamSize diff --git a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs index 6ee81870342..e3edf0d19da 100644 --- a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs @@ -238,7 +238,7 @@ toStoredGroup scimBaseUri ug = Meta.WithMeta meta (Common.WithId ug.id_ sg) typ = "User", ref = Common.uriToText . mkLocation $ "/Users/" <> idToString uid } - | uid <- toList (runIdentity ug.members) + | uid <- toList (runIdentity ug.members) ] } diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index b28e38dfc52..7af4b25c9a5 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -101,8 +101,7 @@ getAllTeamCollaboratorsImpl zUser team = do Store.getAllTeamCollaborators team internalGetTeamCollaboratorsWithIdsImpl :: - ( Member Store.TeamCollaboratorsStore r - ) => + (Member Store.TeamCollaboratorsStore r) => Set TeamId -> Set UserId -> Sem r [TeamCollaborator] @@ -110,8 +109,7 @@ internalGetTeamCollaboratorsWithIdsImpl = do Store.getTeamCollaboratorsWithIds internalUpdateTeamCollaboratorImpl :: - ( Member Store.TeamCollaboratorsStore r - ) => + (Member Store.TeamCollaboratorsStore r) => UserId -> TeamId -> Set CollaboratorPermission -> @@ -120,8 +118,7 @@ internalUpdateTeamCollaboratorImpl user team perms = do Store.updateTeamCollaborator user team perms internalRemoveTeamCollaboratorImpl :: - ( Member Store.TeamCollaboratorsStore r - ) => + (Member Store.TeamCollaboratorsStore r) => UserId -> TeamId -> Sem r () diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs index da795e73daf..f864cd02e23 100644 --- a/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs @@ -53,7 +53,7 @@ generateTeamEvents uid tid eventsData = do { recipientUserId = u, recipientClients = RecipientClientsAll } - | u <- admins ^.. TM.teamMembers . traverse . TM.userId + | u <- admins ^.. TM.teamMembers . traverse . TM.userId ], transient = False } diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs index 997f3fb6ffc..ce4e3ae515e 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs @@ -223,8 +223,7 @@ getUserGroup team id_ includeChannels = do getUserGroupsWithMembers :: forall r. - ( UserGroupStorePostgresEffectConstraints r - ) => + (UserGroupStorePostgresEffectConstraints r) => TeamId -> UserGroupPageRequest -> Sem r UserGroupPageWithMembers diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs index 1ed9de0bc2d..20b9ea71d94 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs @@ -261,8 +261,7 @@ getUserGroups getter pageReq = do getUserGroupsInternal :: forall r. - ( Member Store.UserGroupStore r - ) => + (Member Store.UserGroupStore r) => TeamId -> Maybe Text -> Maybe ManagedBy -> diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs index 67773aa5cb9..dfb21478e8c 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs @@ -40,8 +40,7 @@ emailKeyToCode = . show inMemoryActivationCodeStoreInterpreter :: - ( Member (State (Map EmailKey (Maybe UserId, ActivationCode))) r - ) => + (Member (State (Map EmailKey (Maybe UserId, ActivationCode))) r) => InterpreterFor ActivationCodeStore r inMemoryActivationCodeStoreInterpreter = interpret \case LookupActivationCode ek -> gets (!? ek) diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 4af0f52d434..edfb8af1f83 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -102,7 +102,7 @@ spec = describe "UserSubsystem.Interpreter" do Nothing (mkUserFromStored domain miniLocale targetUser) defUserLegalHoldStatus - | targetUser <- users + | targetUser <- users ] expectedLocalProfiles = mkExpectedProfiles localDomain localTargetUsers expectedProfiles1 = mkExpectedProfiles remoteDomain1 targetUsers1 diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 999ec485d6b..c5303cec00f 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -422,9 +422,7 @@ let pkgs.kubelogin-oidc pkgs.nixpkgs-fmt pkgs.openssl - # FUTUREWORK: we are temporarily using the old ormolu version to prevent the noise of reformatting a lot of code - # upgrade ormolu and reformat in a follow up PR - pkgs.haskell.packages.ghc98.ormolu + pkgs.haskellPackages.ormolu pkgs.vacuum-go pkgs.shellcheck pkgs.treefmt diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 69e76cc4e7e..fc0d6a62e90 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -358,8 +358,7 @@ authAPI = ) federationRemotesAPI :: - ( Member FederationConfigStore r - ) => + (Member FederationConfigStore r) => ServerT BrigIRoutes.FederationRemotesAPI (Handler r) federationRemotesAPI = Named @"add-federation-remotes" addFederationRemote @@ -378,8 +377,7 @@ getFederationRemoteTeams domain = lift $ liftSem $ E.getFederationRemoteTeams domain addFederationRemoteTeam :: - ( Member FederationConfigStore r - ) => + (Member FederationConfigStore r) => Domain -> FederationRemoteTeam -> (Handler r) () @@ -398,8 +396,7 @@ getFederationRemotes :: (Member FederationConfigStore r) => (Handler r) Federati getFederationRemotes = lift $ liftSem $ E.getFederationConfigs addFederationRemote :: - ( Member FederationConfigStore r - ) => + (Member FederationConfigStore r) => FederationDomainConfig -> (Handler r) () addFederationRemote fedDomConf = do @@ -775,15 +772,13 @@ getActivationCode email = do maybe (throwStd activationKeyNotFound) (pure . GetActivationCodeResp) apair getPasswordResetCodeH :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => EmailAddress -> Handler r GetPasswordResetCodeResp getPasswordResetCodeH email = getPasswordResetCode email getPasswordResetCode :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => EmailAddress -> Handler r GetPasswordResetCodeResp getPasswordResetCode email = @@ -1003,8 +998,7 @@ getAccountsByInternalH getByData = do lift . liftSem $ getAccountsBy (qualifyAs loc getByData) createGroupInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => CreateGroupInternalRequest -> Handler r UserGroup createGroupInternalH req = @@ -1016,8 +1010,7 @@ createGroupInternalH req = req.newGroup getGroupInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => TeamId -> UserGroupId -> Bool -> @@ -1026,8 +1019,7 @@ getGroupInternalH tid uid includeChannels = lift . liftSem $ getGroupInternal tid uid includeChannels getGroupsInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => TeamId -> Maybe T.Text -> Maybe ManagedBy -> @@ -1038,16 +1030,14 @@ getGroupsInternalH tid nameContains managedBy startIndex mbCount = lift . liftSem $ getGroupsInternal tid nameContains managedBy startIndex mbCount updateGroupInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => UpdateGroupInternalRequest -> Handler r () updateGroupInternalH req = lift . liftSem $ resetUserGroupInternal req deleteGroupManagedInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => TeamId -> UserGroupId -> ManagedBy -> diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 99202db89c8..4fcdefe883d 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -309,7 +309,7 @@ versionedSwaggerDocsAPI Nothing = tocPage renderLink "swagger.json" ("/" <> v <> "/api/swagger.json"), "
" ] - | v <- versionToLByteString <$> [minBound :: Version ..] + | v <- versionToLByteString <$> [minBound :: Version ..] ] internal :: [LByteString] @@ -325,7 +325,7 @@ versionedSwaggerDocsAPI Nothing = tocPage renderLink "swagger.json" ("/api-internal/swagger-ui/" <> s <> "-swagger.json"), "
" ] - | s <- ["brig", "galley", "spar", "cargohold", "gundeck", "cannon", "proxy"] + | s <- ["brig", "galley", "spar", "cargohold", "gundeck", "cannon", "proxy"] ] federated :: [LByteString] @@ -338,10 +338,10 @@ versionedSwaggerDocsAPI Nothing = tocPage renderLink "swagger.json" ("/" <> v <> "/api-federation/swagger-ui/" <> s <> "-swagger.json"), "
" ] - | v <- versionToLByteString <$> [minBound :: Fed.Version ..] + | v <- versionToLByteString <$> [minBound :: Fed.Version ..] ] <> "
" - | s <- ["brig", "galley", "cargohold"] + | s <- ["brig", "galley", "cargohold"] ] versionToLByteString :: (ToHttpApiData v) => v -> LByteString @@ -1249,8 +1249,7 @@ beginPasswordReset (Public.NewPasswordReset target) = lift (liftSem $ createPasswordResetCode $ mkEmailKey target) completePasswordReset :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => Public.CompletePasswordReset -> Handler r () completePasswordReset req = do @@ -1769,8 +1768,7 @@ deprecatedOnboarding :: UserId -> JsonValue -> (Handler r) DeprecatedMatchingRes deprecatedOnboarding _ _ = pure DeprecatedMatchingResult deprecatedCompletePasswordReset :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => Public.PasswordResetKey -> Public.PasswordReset -> (Handler r) () diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index b2efc000f97..7c0397cefee 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -397,8 +397,7 @@ createUser rateLimitKey new = do lift $ do mHashedPassword <- traverse - ( liftSem . HashPassword.hashPassword8 rateLimitKey - ) + (liftSem . HashPassword.hashPassword8 rateLimitKey) new'.newUserPassword newStoredUser new' {newUserPassword = mHashedPassword} mbInv tid mbHandle domain <- viewFederationDomain @@ -1101,8 +1100,7 @@ lookupActivationCode email = do pure $ (k,) <$> c lookupPasswordResetCode :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => EmailAddress -> (AppT r) (Maybe PasswordResetPair) lookupPasswordResetCode = diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index b32f4b44863..603aeaa5e48 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -725,8 +725,7 @@ updateServiceWhitelist uid con tid upd = do .| C.mapM_ ( pooledMapConcurrentlyN_ 16 - ( uncurry (deleteBot uid (Just con)) - ) + (uncurry (deleteBot uid (Just con))) ) wrapClientE $ DB.deleteServiceWhitelist (Just tid) pid sid pure UpdateServiceWhitelistRespChanged @@ -852,8 +851,7 @@ addBot zuid zcon cid add = do bcl newClt maxPermClients - ( Just $ ClientCapabilityList $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent - ) + (Just $ ClientCapabilityList $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent) ) !>> const (StdError $ badGatewayWith "MalformedPrekeys") diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index b898f49bed6..e1c169b527e 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -401,8 +401,7 @@ testLoginFailure brig = do let badmail = unsafeEmailAddress "wrong" "wire.com" login brig - ( MkLogin (LoginByEmail badmail) defPassword Nothing Nothing - ) + (MkLogin (LoginByEmail badmail) defPassword Nothing Nothing) PersistentCookie !!! const 403 === statusCode diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index f01cb06cb71..a49dace7efe 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -461,8 +461,7 @@ testSendMessage brig1 brig2 galley2 cannon1 = do evtFrom e @?= EventFromUser (userQualifiedId bob) evtData e @?= EdOtrMessage - ( OtrMessage bobClient.clientId aliceClient.clientId (toBase64Text msgText) (Just "") - ) + (OtrMessage bobClient.clientId aliceClient.clientId (toBase64Text msgText) (Just "")) -- alice creates a conversation on domain 1 with bob on domain 2, then bob -- sends a message to alice @@ -524,8 +523,7 @@ testSendMessageToRemoteConv brig1 brig2 galley1 galley2 cannon1 = do evtFrom e @?= EventFromUser (userQualifiedId bob) evtData e @?= EdOtrMessage - ( OtrMessage bobClient.clientId aliceClient.clientId (toBase64Text msgText) (Just "") - ) + (OtrMessage bobClient.clientId aliceClient.clientId (toBase64Text msgText) (Just "")) testDeleteUser :: Brig -> Brig -> Galley -> Galley -> Cannon -> Http () testDeleteUser brig1 brig2 galley1 galley2 cannon1 = do diff --git a/services/galley/src/Galley/API/Action/Notify.hs b/services/galley/src/Galley/API/Action/Notify.hs index edcb90cfc03..2e65778f8bf 100644 --- a/services/galley/src/Galley/API/Action/Notify.hs +++ b/services/galley/src/Galley/API/Action/Notify.hs @@ -33,8 +33,7 @@ import Wire.StoredConversation sendConversationActionNotifications :: forall tag r. - ( Member ConversationSubsystem r - ) => + (Member ConversationSubsystem r) => Sing tag -> Qualified UserId -> Bool -> diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index b6212622b23..5a5cd06db6b 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -1001,8 +1001,7 @@ updateTypingIndicator origDomain TypingDataUpdateRequest {..} = do pure (either TypingDataUpdateError TypingDataUpdateSuccess ret) onTypingIndicatorUpdated :: - ( Member NotificationSubsystem r - ) => + (Member NotificationSubsystem r) => Domain -> TypingDataUpdated -> Sem r EmptyResponse diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index fd6aae3d52f..e8140aa492b 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -512,8 +512,7 @@ guardLegalholdPolicyConflictsH glh = do -- | Get an MLS conversation client list iGetMLSClientListForConv :: forall r. - ( Member ConversationStore r - ) => + (Member ConversationStore r) => GroupId -> Sem r ClientList iGetMLSClientListForConv gid = do diff --git a/services/galley/src/Galley/API/MLS/CheckClients.hs b/services/galley/src/Galley/API/MLS/CheckClients.hs index 9822664de18..31ff97cf2ce 100644 --- a/services/galley/src/Galley/API/MLS/CheckClients.hs +++ b/services/galley/src/Galley/API/MLS/CheckClients.hs @@ -125,8 +125,8 @@ mkClientData clientInfo = infoMap = Map.fromList [ (info.clientId, key) - | info <- toList clientInfo, - key <- toList info.mlsSignatureKey + | info <- toList clientInfo, + key <- toList info.mlsSignatureKey ] } diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs index 241dad0b2e6..a10870a306e 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfo.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -65,8 +65,7 @@ getGroupInfo lusr qcnvId = do qcnvId getGroupInfoFromLocalConv :: - ( Member ConversationStore r - ) => + (Member ConversationStore r) => (Members MLSGroupInfoStaticErrors r) => Qualified UserId -> Local ConvId -> diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 0f99e0a916b..052b8132b0b 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -605,8 +605,7 @@ postMLSMessageToRemoteConv loc qusr senderClient con msg rConvOrSubId = do MLSMessageResponseOutOfSyncError e -> throw e storeGroupInfo :: - ( Member ConversationStore r - ) => + (Member ConversationStore r) => ConvOrSubConvId -> GroupInfoData -> Sem r () diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 7808ea80ed3..488d692aa7a 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -396,5 +396,4 @@ evalGalley e = interpretTeamFeatureSpecialContext :: Env -> Sem (Input (FeatureDefaults LegalholdConfig) ': r) a -> Sem r a interpretTeamFeatureSpecialContext e = runInputConst - ( e ^. options . settings . featureFlags . to npProject - ) + (e ^. options . settings . featureFlags . to npProject) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index e0f65f5d3af..313f11f0f55 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -2661,8 +2661,7 @@ leaveRemoteConvDenied = do guardComponent Galley mockReply $ LeaveConversationResponse - ( Left RemoveFromConversationErrorRemovalNotAllowed - ) + (Left RemoveFromConversationErrorRemovalNotAllowed) (resp, fedRequests) <- withTempMockFederator' mockResponses $ diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs index dd1ae258895..68f9c7ab0e0 100644 --- a/services/galley/test/integration/API/MLS/Mocks.hs +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -49,8 +49,7 @@ receiveCommitMock clients = "get-not-fully-connected-backends" ~> NonConnectedBackends mempty, "get-mls-clients" ~> Set.fromList - ( map (\c -> ClientInfo c.ciClient mempty True) clients - ) + (map (\c -> ClientInfo c.ciClient mempty True) clients) ] receiveCommitMockByDomain :: [ClientIdentity] -> Mock LByteString diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 8d6a496995f..ef96d88d206 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -1165,8 +1165,7 @@ leaveCurrentConv cid qsub = case qUnqualified qsub of leaveSubConv (ciUser cid) (ciClient cid) (qsub $> cnv) subId !!! const 200 === statusCode ) - ( \rcid -> remoteLeaveCurrentConv rcid (qsub $> cnv) subId - ) + (\rcid -> remoteLeaveCurrentConv rcid (qsub $> cnv) subId) (cidQualifiedUser cid $> cid) State.modify $ \mls -> mls diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 9807e6756fe..6c096a04174 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -865,8 +865,7 @@ testDeleteBindingTeamSingleMember = do . zUser owner . zConn "conn" . json - ( newTeamMemberDeleteData (Just Util.defPassword) - ) + (newTeamMemberDeleteData (Just Util.defPassword)) ) !!! const 202 === statusCode diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index aaef7736187..cb7b4f5fa88 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -606,8 +606,8 @@ mockBulkPush notifs = do let delivered :: [(Notification, [Presence])] delivered = [ (nid, prcs) - | (nid, filter (`elem` deliveredprcs) -> prcs) <- notifs, - not $ null prcs -- (sic!) (this is what gundeck currently does) + | (nid, filter (`elem` deliveredprcs) -> prcs) <- notifs, + not $ null prcs -- (sic!) (this is what gundeck currently does) ] deliveredprcs :: [Presence] deliveredprcs = filter isreachable . mconcat . fmap fakePresences $ allRecipients env diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 58e313646e6..f5f9de0d1e9 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -889,7 +889,7 @@ validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (J ( case fromMaybe defWireIdPAPIVersion $ previousIdP ^. SAML.idpExtraInfo . apiVersion of WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe newIssuer WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2Maybe newIssuer teamId - ) + ) <&> ( \case Just idpFound -> idpFound ^. SAML.idpId /= _idpId Nothing -> False diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 0212cbd97a1..6c1735c98b3 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -979,8 +979,8 @@ specCRUDIdentityProvider = do describe "replaces an existing idp" $ forM_ [ (u, e) - | u <- [False, True], -- do we use update-by-put or update-by-post? (see below) - e <- [False, True] -- is the externalId an email address? (if not, it's a uuidv4, and the email address is stored in `emails`) + | u <- [False, True], -- do we use update-by-put or update-by-post? (see below) + e <- [False, True] -- is the externalId an email address? (if not, it's a uuidv4, and the email address is stored in `emails`) ] $ \(updateNotReplace, externalIdIsEmail) -> do let updateOrReplaceIdps :: (UserId, IdP, SAML.IdPMetadata) -> TestSpar () @@ -1466,8 +1466,7 @@ specSsoSettings = do callGetDefaultSsoCode (env ^. teSpar) `shouldRespondWith` \resp -> (statusCode resp == 200) - && ( responseJsonEither resp == Right (ssoSettings (Just idpid1)) - ) + && (responseJsonEither resp == Right (ssoSettings (Just idpid1))) -- update to 2 callSetDefaultSsoCode (env ^. teSpar) idpid2 `shouldRespondWith` \resp -> @@ -1476,8 +1475,7 @@ specSsoSettings = do callGetDefaultSsoCode (env ^. teSpar) `shouldRespondWith` \resp -> (statusCode resp == 200) - && ( responseJsonEither resp == Right (ssoSettings (Just idpid2)) - ) + && (responseJsonEither resp == Right (ssoSettings (Just idpid2))) it "allows removing the default SSO code" $ do env <- ask (userid, _teamid) <- callCreateUserWithTeam @@ -1494,8 +1492,7 @@ specSsoSettings = do callGetDefaultSsoCode (env ^. teSpar) `shouldRespondWith` \resp -> (statusCode resp == 200) - && ( responseJsonEither resp == Right (ssoSettings Nothing) - ) + && (responseJsonEither resp == Right (ssoSettings Nothing)) it "removes the default SSO code if the IdP gets removed" $ do env <- ask (userid, _teamid) <- callCreateUserWithTeam @@ -1511,8 +1508,7 @@ specSsoSettings = do callGetDefaultSsoCode (env ^. teSpar) `shouldRespondWith` \resp -> (statusCode resp == 200) - && ( responseJsonEither resp == Right (ssoSettings Nothing) - ) + && (responseJsonEither resp == Right (ssoSettings Nothing)) where ssoSettings maybeCode = object diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 98e262a6531..0ba12df2135 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -455,7 +455,7 @@ getUserData uid mMaxConvs mMaxNotifs = do notfs <- ( Intra.getUserNotifications uid (fromMaybe 100 mMaxNotifs) <&> toJSON @[QueuedNotification] - ) + ) `catchE` (pure . String . T.pack . show) -- galeb From e76f5005be15c0a05a4f2136e1d57f86acb55e5c Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 6 Jan 2026 09:27:30 +0100 Subject: [PATCH 43/60] Fix HLS setup: Allow newer version of lib:Cabal in proto-lens-setup (#4930) * nix: Stop overriding version of Cabal, its not needed anymore * cabal.project: Allow proto-lens-setup to use newer Cabal Explanation in comment. Partially reverts https://github.com/wireapp/wire-server/pull/4928 * Remove stale changelog --- cabal.project | 16 ++++++++++++++++ .../5-internal/remove-protoc-cabal-override | 1 - nix/manual-overrides.nix | 6 ------ 3 files changed, 16 insertions(+), 7 deletions(-) delete mode 100644 changelog.d/5-internal/remove-protoc-cabal-override diff --git a/cabal.project b/cabal.project index 8ef7a564743..d8ea6ac6a60 100644 --- a/cabal.project +++ b/cabal.project @@ -68,3 +68,19 @@ benchmarks: True program-options ghc-options: -Werror + +-- NOTE: +-- +-- When nix builds proto-lens-setup, it builds it with Cabal-3.12 which is the +-- default lib:Cabal in ghc-9.10. However, this proto-lens-setup doesn't really +-- get used when running build with cabal, and for some reason cabal reinstalls +-- it from hackage. +-- +-- We use cabal-install-3.16, when hie-bios detects it is using a version >= +-- 3.15, it uses the `--with-repl` option. This option only works when custom +-- setups don't use older versions of `Cabal`. This makes HLS not work at all. +-- +-- Adding this allow-newer option makes cabal compile proto-lens-setup with +-- newer version of lib:Cabal. +allow-newer: + , proto-lens-setup:Cabal diff --git a/changelog.d/5-internal/remove-protoc-cabal-override b/changelog.d/5-internal/remove-protoc-cabal-override deleted file mode 100644 index 9db8396da96..00000000000 --- a/changelog.d/5-internal/remove-protoc-cabal-override +++ /dev/null @@ -1 +0,0 @@ -Allowing any newer version of protoc led to issues running the Haskell Language Server (HLS). This override in `cabal.project` has now been removed. A newer version than the one available via Nix shouldn't be required. diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 5ae39056828..764b1b268e2 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -77,12 +77,6 @@ hself: hsuper: { # warp requires curl in its testsuite warp = hlib.addTestToolDepends hsuper.warp [ curl ]; - # cabal multirepl requires Cabal 3.12 - Cabal = hsuper.Cabal_3_12_1_0; - Cabal-syntax = hsuper.Cabal-syntax_3_14_2_0; - - text-builder = hlib.doJailbreak hsuper.text-builder; # uses 1.0.0.4 from nixpkgs - # ----------------- # flags and patches # (these are fine) From 2a97f62cadbf48a2bc0929c3ef4858e9f4e6e376 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 6 Jan 2026 15:36:16 +0100 Subject: [PATCH 44/60] Fix HLS Setup: use cabal-install-3.12, disable hlint plugin and hide build-tool-depends when using cabal.project to compile (#4932) * nix/overlay.nix: Delete dead code * HLS: Disable hlint https://github.com/haskell/haskell-language-server/issues/4674 * cabal: Allow hiding build-tool-depends when compiling in nix dev env https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 * nix: Use cabal-install 3.12 HLS doesn't like it when cabal-install is 3.16 but all the custom setups use lib:Cabal <= 3.14. Since GHC 9.10 comes with lib:Cabal 3.12, things don't work so well. --- cabal.project | 44 ++++++++----- libs/dns-util/dns-util.cabal | 12 +++- libs/extended/extended.cabal | 12 +++- libs/hscim/hscim.cabal | 12 +++- libs/http2-manager/http2-manager.cabal | 19 ++++-- libs/metrics-wai/metrics-wai.cabal | 12 +++- .../polysemy-wire-zoo/polysemy-wire-zoo.cabal | 12 +++- .../types-common-journal.cabal | 13 +++- libs/wai-utilities/wai-utilities.cabal | 24 +++++-- .../wire-api-federation.cabal | 12 +++- .../wire-message-proto-lens.cabal | 12 +++- libs/wire-subsystems/wire-subsystems.cabal | 22 +++++-- nix/default.nix | 4 +- nix/overlay.nix | 66 ++++--------------- nix/sources.json | 12 ++++ nix/wire-server.nix | 3 +- services/spar/spar.cabal | 16 ++++- services/wire-server-enterprise | 2 +- 18 files changed, 210 insertions(+), 99 deletions(-) diff --git a/cabal.project b/cabal.project index d8ea6ac6a60..f35b3b95050 100644 --- a/cabal.project +++ b/cabal.project @@ -69,18 +69,32 @@ benchmarks: True program-options ghc-options: -Werror --- NOTE: --- --- When nix builds proto-lens-setup, it builds it with Cabal-3.12 which is the --- default lib:Cabal in ghc-9.10. However, this proto-lens-setup doesn't really --- get used when running build with cabal, and for some reason cabal reinstalls --- it from hackage. --- --- We use cabal-install-3.16, when hie-bios detects it is using a version >= --- 3.15, it uses the `--with-repl` option. This option only works when custom --- setups don't use older versions of `Cabal`. This makes HLS not work at all. --- --- Adding this allow-newer option makes cabal compile proto-lens-setup with --- newer version of lib:Cabal. -allow-newer: - , proto-lens-setup:Cabal +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +package polysemy-wire-zoo + flags: +nix-dev-env +package dns-util + flags: +nix-dev-env +package wire-subsystems + flags: +nix-dev-env +package wai-utilities + flags: +nix-dev-env +package wire-api-federation + flags: +nix-dev-env +package http2-manager + flags: +nix-dev-env +package hscim + flags: +nix-dev-env +package extended + flags: +nix-dev-env +package metrics-wai + flags: +nix-dev-env +package wire-server-enterprise + flags: +nix-dev-env +package spar + flags: +nix-dev-env +package wire-message-proto-lens + flags: +nix-dev-env +package types-common-journal + flags: +nix-dev-env diff --git a/libs/dns-util/dns-util.cabal b/libs/dns-util/dns-util.cabal index 4cfa56f240e..7b120e36e1c 100644 --- a/libs/dns-util/dns-util.cabal +++ b/libs/dns-util/dns-util.cabal @@ -11,6 +11,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Wire.Network.DNS.Effect @@ -132,7 +140,9 @@ test-suite spec -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints -Wunused-packages -Wno-x-partial - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: base >=4.6 && <5.0 , dns diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index a771fe15901..3828324caa2 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -16,6 +16,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -172,7 +180,9 @@ test-suite extended-tests -threaded -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson , base diff --git a/libs/hscim/hscim.cabal b/libs/hscim/hscim.cabal index 73814d4a55e..96d62972d02 100644 --- a/libs/hscim/hscim.cabal +++ b/libs/hscim/hscim.cabal @@ -25,6 +25,14 @@ source-repository head location: https://github.com/wireapp/wire-server subdir: libs/hscim +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Web.Scim.AttrName @@ -207,7 +215,9 @@ test-suite spec -Wall -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson , attoparsec diff --git a/libs/http2-manager/http2-manager.cabal b/libs/http2-manager/http2-manager.cabal index 3aed0c465ba..87d6d58241e 100644 --- a/libs/http2-manager/http2-manager.cabal +++ b/libs/http2-manager/http2-manager.cabal @@ -20,6 +20,14 @@ extra-source-files: test/resources/unit-ca-key.pem test/resources/unit-ca.pem +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -53,8 +61,8 @@ flag test-trailing-dot default: True test-suite http2-manager-tests - type: exitcode-stdio-1.0 - main-is: Main.hs + type: exitcode-stdio-1.0 + main-is: Main.hs ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path @@ -69,8 +77,11 @@ test-suite http2-manager-tests Main Test.HTTP2.Client.ManagerSpec - hs-source-dirs: test - build-tool-depends: hspec-discover:hspec-discover + hs-source-dirs: test + + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: async , base diff --git a/libs/metrics-wai/metrics-wai.cabal b/libs/metrics-wai/metrics-wai.cabal index 78002563cec..53a354f59e4 100644 --- a/libs/metrics-wai/metrics-wai.cabal +++ b/libs/metrics-wai/metrics-wai.cabal @@ -10,6 +10,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Data.Metrics.Middleware.Prometheus @@ -138,7 +146,9 @@ test-suite unit -threaded -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: base >=4 && <5 , containers diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index 87dc102d657..cc89c97c7c1 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -10,6 +10,14 @@ copyright: (c) 2020 Wire Swiss GmbH license: AGPL-3 build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -164,7 +172,9 @@ test-suite spec -Wno-redundant-constraints -Werror -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: base , containers diff --git a/libs/types-common-journal/types-common-journal.cabal b/libs/types-common-journal/types-common-journal.cabal index 958378b2367..d4ffce94fbf 100644 --- a/libs/types-common-journal/types-common-journal.cabal +++ b/libs/types-common-journal/types-common-journal.cabal @@ -20,6 +20,14 @@ custom-setup , Cabal >=3.12 , proto-lens-setup +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Data.Proto @@ -80,7 +88,10 @@ library -Wredundant-constraints -Wunused-packages ghc-prof-options: -fprof-auto-exported - build-tool-depends: proto-lens-protoc:proto-lens-protoc + + if !flag(nix-dev-env) + build-tool-depends: proto-lens-protoc:proto-lens-protoc + build-depends: base >=4 && <5 , bytestring diff --git a/libs/wai-utilities/wai-utilities.cabal b/libs/wai-utilities/wai-utilities.cabal index 9faf8a2ed40..d600a3c4634 100644 --- a/libs/wai-utilities/wai-utilities.cabal +++ b/libs/wai-utilities/wai-utilities.cabal @@ -11,6 +11,14 @@ license: AGPL-3.0-only license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + common common-all default-language: GHC2021 ghc-options: @@ -106,15 +114,17 @@ library , warp-tls test-suite wai-utilities-tests - import: common-all - type: exitcode-stdio-1.0 - main-is: Main.hs - ghc-options: -threaded -with-rtsopts=-N - hs-source-dirs: test - build-tool-depends: hspec-discover:hspec-discover + import: common-all + type: exitcode-stdio-1.0 + main-is: Main.hs + ghc-options: -threaded -with-rtsopts=-N + hs-source-dirs: test + + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover -- cabal-fmt: expand test -Main - other-modules: Network.Wai.Utilities.ServerSpec + other-modules: Network.Wai.Utilities.ServerSpec build-depends: , bytestring , hspec diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 00a2f9dd718..a451e2f01c0 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -13,6 +13,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -191,7 +199,9 @@ test-suite spec -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson >=2.0.1.0 , aeson-pretty diff --git a/libs/wire-message-proto-lens/wire-message-proto-lens.cabal b/libs/wire-message-proto-lens/wire-message-proto-lens.cabal index 8087bfa00f8..54e3e907ead 100644 --- a/libs/wire-message-proto-lens/wire-message-proto-lens.cabal +++ b/libs/wire-message-proto-lens/wire-message-proto-lens.cabal @@ -19,6 +19,14 @@ custom-setup , Cabal >=3.12 , proto-lens-setup +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Proto.Mls @@ -81,7 +89,9 @@ library base , proto-lens-runtime - build-tool-depends: proto-lens-protoc:proto-lens-protoc + if !flag(nix-dev-env) + build-tool-depends: proto-lens-protoc:proto-lens-protoc + default-language: GHC2021 autogen-modules: Proto.Otr diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index f8ea7fdff63..ff58ed19431 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -21,6 +21,14 @@ extra-source-files: test/resources/**/*.txt test/resources/**/*.yaml +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + common common-all default-language: GHC2021 ghc-options: @@ -439,14 +447,14 @@ library , zauth test-suite wire-subsystems-tests - import: common-all - type: exitcode-stdio-1.0 + import: common-all + type: exitcode-stdio-1.0 -- include everything in source dirs we want to watch when running -- `ghcid --command 'cabal repl test:wire-subsystems-tests' --test='main'`. - hs-source-dirs: test/unit - main-is: ../Main.hs - ghc-options: -fplugin=Polysemy.Plugin -Wno-x-partial -threaded + hs-source-dirs: test/unit + main-is: ../Main.hs + ghc-options: -fplugin=Polysemy.Plugin -Wno-x-partial -threaded -- cabal-fmt: expand test/unit other-modules: @@ -504,7 +512,9 @@ test-suite wire-subsystems-tests Wire.Util Wire.VerificationCodeSubsystem.InterpreterSpec - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: , hspec , QuickCheck diff --git a/nix/default.nix b/nix/default.nix index b945b4adc9a..4d731d0573d 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -10,6 +10,8 @@ let ]; }; + pkgs_24_11 = import sources.nixpkgs_24_11 { }; + profileEnv = pkgs.writeTextFile { name = "profile-env"; destination = "/.profile"; @@ -20,7 +22,7 @@ let ''; }; - wireServer = import ./wire-server.nix pkgs; + wireServer = import ./wire-server.nix pkgs pkgs_24_11; nginz = pkgs.callPackage ./nginz.nix { }; # packages necessary to build wire-server docs diff --git a/nix/overlay.nix b/nix/overlay.nix index 36af565994b..2ccc18888e2 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -1,57 +1,3 @@ -let - staticBinaryInTarball = { stdenv, fetchurl, pname, version, linuxAmd64Url, linuxAmd64Sha256, darwinAmd64Url, darwinAmd64Sha256, binPath ? pname }: - stdenv.mkDerivation { - inherit pname version; - - src = - if stdenv.isDarwin - then - fetchurl - { - url = darwinAmd64Url; - sha256 = darwinAmd64Sha256; - } - else - fetchurl { - url = linuxAmd64Url; - sha256 = linuxAmd64Sha256; - }; - - installPhase = '' - mkdir -p $out/bin - cp ${binPath} $out/bin - ''; - }; - - staticBinary = { stdenv, fetchurl, pname, version, linuxAmd64Url, linuxAmd64Sha256, darwinAmd64Url, darwinAmd64Sha256, binPath ? pname }: - stdenv.mkDerivation { - inherit pname version; - - src = - if stdenv.isDarwin - then - fetchurl - { - url = darwinAmd64Url; - sha256 = darwinAmd64Sha256; - } - else - fetchurl { - url = linuxAmd64Url; - sha256 = linuxAmd64Sha256; - }; - phases = [ "installPhase" "patchPhase" ]; - - installPhase = '' - mkdir -p $out/bin - cp $src $out/bin/${binPath} - chmod +x $out/bin/${binPath} - ''; - }; - - sources = import ./sources.nix; -in - self: super: { cryptobox = self.callPackage ./pkgs/cryptobox { }; @@ -83,4 +29,16 @@ self: super: { rabbitmqadmin = super.callPackage ./pkgs/rabbitmqadmin { }; sbomqs = super.callPackage ./pkgs/sbomqs { }; + + # Disable hlint in HLS to get around this bug: + # https://github.com/haskell/haskell-language-server/issues/4674 + haskell = super.haskell // { + packages = super.haskell.packages // { + ghc910 = super.haskell.packages.ghc910.override { + overrides = hfinal: hprev: { + haskell-language-server = self.haskell.lib.disableCabalFlag hprev.haskell-language-server "hlint"; + }; + }; + }; + }; } diff --git a/nix/sources.json b/nix/sources.json index cc357e99b48..60c5cfb6df8 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -10,5 +10,17 @@ "type": "tarball", "url": "https://github.com/NixOS/nixpkgs/archive/09b8fda8959d761445f12b55f380d90375a1d6bb.tar.gz", "url_template": "https://github.com///archive/.tar.gz" + }, + "nixpkgs_24_11": { + "branch": "nixos-24.11", + "description": "Nix Packages collection & NixOS", + "homepage": "", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", + "sha256": "1s2gr5rcyqvpr58vxdcb095mdhblij9bfzaximrva2243aal3dgx", + "type": "tarball", + "url": "https://github.com/nixos/nixpkgs/archive/50ab793786d9de88ee30ec4e4c24fb4236fc2674.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index c5303cec00f..d360bff24c4 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -43,6 +43,7 @@ # components and the required dependencies. We then use this package set along # with nixpkgs' dockerTools to make derivations for docker images that we need. pkgs: +pkgs_24_11: let inherit (pkgs) lib; hlib = pkgs.haskell.lib; @@ -526,7 +527,7 @@ in pkgs.sbomqs pkgs.postgresql - pkgs.cabal-install + pkgs_24_11.cabal-install pkgs.nix-prefetch-git pkgs.haskellPackages.cabal-plan pkgs.lsof diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index c9637fca4f4..6d26c0d0f07 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -13,6 +13,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -340,7 +348,9 @@ executable spar-integration -with-rtsopts=-N -Wredundant-constraints -Wunused-packages -Wno-x-partial - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson , aeson-qq @@ -612,7 +622,9 @@ test-suite spec -with-rtsopts=-N -Wredundant-constraints -Wunused-packages -Wno-x-partial - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson , aeson-qq diff --git a/services/wire-server-enterprise b/services/wire-server-enterprise index b4419041fc3..8950f728178 160000 --- a/services/wire-server-enterprise +++ b/services/wire-server-enterprise @@ -1 +1 @@ -Subproject commit b4419041fc31ca260accec9d4f0f019bfd53c077 +Subproject commit 8950f728178bc7d16c83303f2aa66a6321f3c29e From 74575f346015ef4a2c7cca6200179a637d1faf10 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 7 Jan 2026 08:44:25 +0100 Subject: [PATCH 45/60] Make `make clean` honor $(package). (#4924) --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index ecf42013455..10cb677a951 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,12 @@ full-clean: clean .PHONY: clean clean: +ifeq ("$(package)", "all") cabal clean +else + -if ( test -e dist || test -e dist-newstyle ); then find dist* -type d -name '$(package)-*' -exec rm -rf {}; fi +endif + # `/dist` shouldn't be created or used by anybody any more, we're just making sure here. -rm -rf dist -rm -f "bill-of-materials.$(HELM_SEMVER).json" From a16df878df779f545a5db26138dc2cfc45f52404 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Wed, 7 Jan 2026 09:09:26 +0100 Subject: [PATCH 46/60] Add IdP golden test (#4927) The IdP entity is (de-) serialized in requests and thus should have golden tests to ensure the format doesn't change. --- changelog.d/5-internal/add-IdP-golden-test | 1 + .../golden/Test/Wire/API/Golden/Manual.hs | 8 + .../golden/Test/Wire/API/Golden/Manual/IdP.hs | 252 ++++++++++++++++++ .../test/golden/testObject_IdP_1.json | 22 ++ .../test/golden/testObject_IdP_2.json | 18 ++ .../test/golden/testObject_IdP_3.json | 21 ++ libs/wire-api/wire-api.cabal | 1 + 7 files changed, 323 insertions(+) create mode 100644 changelog.d/5-internal/add-IdP-golden-test create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/IdP.hs create mode 100644 libs/wire-api/test/golden/testObject_IdP_1.json create mode 100644 libs/wire-api/test/golden/testObject_IdP_2.json create mode 100644 libs/wire-api/test/golden/testObject_IdP_3.json diff --git a/changelog.d/5-internal/add-IdP-golden-test b/changelog.d/5-internal/add-IdP-golden-test new file mode 100644 index 00000000000..3da9806f2b0 --- /dev/null +++ b/changelog.d/5-internal/add-IdP-golden-test @@ -0,0 +1 @@ +Add a golden test for `IdP` (de-) serialization to ensure the format doesn't change due to future developments. diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index f62e490ed21..7cb4d14144b 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -41,6 +41,7 @@ import Test.Wire.API.Golden.Manual.FederationRestriction import Test.Wire.API.Golden.Manual.FederationStatus import Test.Wire.API.Golden.Manual.GetPaginatedConversationIds import Test.Wire.API.Golden.Manual.GroupId +import Test.Wire.API.Golden.Manual.IdP import Test.Wire.API.Golden.Manual.InvitationUserView import Test.Wire.API.Golden.Manual.ListConversations import Test.Wire.API.Golden.Manual.ListUsersById @@ -429,5 +430,12 @@ tests = (testObject_DomainRedirectConfig_2, "testObject_DomainRedirectConfig_2.json"), (testObject_DomainRedirectConfig_4, "testObject_DomainRedirectConfig_4.json") ] + ], + testGroup + "IdP" + $ testObjects + [ (testObject_IdP_1, "testObject_IdP_1.json"), + (testObject_IdP_2, "testObject_IdP_2.json"), + (testObject_IdP_3, "testObject_IdP_3.json") ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/IdP.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/IdP.hs new file mode 100644 index 00000000000..cae4d041bd5 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/IdP.hs @@ -0,0 +1,252 @@ +module Test.Wire.API.Golden.Manual.IdP where + +import Data.Id +import Data.List.NonEmpty +import Data.UUID +import Imports +import SAML2.WebSSO.Types +import Text.XML.DSig +import URI.ByteString +import Wire.API.User.IdentityProvider + +testObject_IdP_1 :: IdP +testObject_IdP_1 = + IdPConfig + { _idpId = IdPId {fromIdPId = (fromJust . Data.UUID.fromString) "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _idpMetadata = + IdPMetadata + { _edIssuer = + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "liisa.kaisa"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + _edRequestURI = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "johanna.leks"}, + authorityPort = Nothing + } + ), + uriPath = "/aytamah", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + }, + _edCertAuthnResponse = + either + error + id + (parseKeyInfo False "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk") + :| [] + }, + _idpExtraInfo = + WireIdP + { _team = (either error id . parseIdFromText) "fc5f3bf8-c296-69e7-27fd-70d483740fe4", + _apiVersion = Nothing, + _oldIssuers = + [ Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "hele.johanna"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "ulli.jannis"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "reet.loviise"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + } + ], + _replacedBy = Just (IdPId {fromIdPId = (fromJust . Data.UUID.fromString) "fc5f3bf8-c296-69e7-27fd-70d483740fe4"}), + _handle = IdPHandle {unIdPHandle = "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _domain = Just "wire.com" + } + } + +testObject_IdP_2 :: IdP +testObject_IdP_2 = + IdPConfig + { _idpId = IdPId {fromIdPId = (fromJust . Data.UUID.fromString) "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _idpMetadata = + IdPMetadata + { _edIssuer = + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "liisa.kaisa"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + _edRequestURI = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "johanna.leks"}, + authorityPort = Nothing + } + ), + uriPath = "/aytamah", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + }, + _edCertAuthnResponse = + either + error + id + (parseKeyInfo False "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk") + :| [] + }, + _idpExtraInfo = + WireIdP + { _team = (either error id . parseIdFromText) "fc5f3bf8-c296-69e7-27fd-70d483740fe4", + _apiVersion = Just WireIdPAPIV2, + _oldIssuers = [], + _replacedBy = Nothing, + _handle = IdPHandle {unIdPHandle = "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _domain = Nothing + } + } + +testObject_IdP_3 :: IdP +testObject_IdP_3 = + let rightOrError :: Either String b -> b + rightOrError = either error id + certs = + rightOrError + <$> (parseKeyInfo False "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk") + :| [(parseKeyInfo False "MIIDpDCCAoygAwIBAgIGAWSx7x1HMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi01MDA1MDgxHDAaBgkqhkiG9w0BCQEWDWluZm9Ab2t0YS5jb20wHhcNMTgwNzE5MDk0NTM1WhcNMjgwNzE5MDk0NjM0WjCBkjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNVBAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtNTAwNTA4MRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhUaQm/3dgPws1A5IjFK9ZQpj170vIqENuDG0tapAzkvk6+9vyhduGckHTeZF3k5MMlW9iix2Eg0qa1oS/Wrq/aBf7+BH6y1MJlQnaKQ3hPL+OFvYzbnrN8k2uC2LivP7Y90dXwtN3P63rA4QSyDPYEMvdKSubUKX/HNsUg4I2PwHmpfWBNgoMkqe0bxQILBv+84L62IYSd6k77XXnCFb/usHpG/gY6sJsTQ2aFl9FuJ51uf67AOj8RzPXstgtUaXbdJI0kAqKIb3j9Zv3mpPCy/GHnyB3PMalvtc1uaz1ZnwO2eliqhwB6/8W6CPutFo1Bhq1glQIX+1OD7906iORwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB0h6vKAywJwH3g0RnocOpBvT42QW57TZ3Wzm9gbg6dQL0rB+NHDx2V0VIh51E3YHL1os9W09MreM7I74D/fX27r1Q3+qAsL1v3CN8WIVh9eYitBCtF7DwZmL2UXTia+GWPrabO14qAztFmTXfqNuCZej7gJd/K2r0KBiZtZ6o58WBREW2F70a6nN6Nk1yjzBkDTJMMf8OMXHphTaalMBXojN9W6HEDpGBE0qY7c70PqvfUEzd8wHWcDxo6+3jajajelk0V4rg7Cqxccr+WwjYtENEuQypNG2mbI52iPZked0QWKy0WzhSMw5wjJ+QDG31vJInAB2769C2KmhPDyNhU")] + in IdPConfig + { _idpId = IdPId {fromIdPId = (fromJust . Data.UUID.fromString) "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _idpMetadata = + IdPMetadata + { _edIssuer = + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "liisa.kaisa"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + _edRequestURI = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "johanna.leks"}, + authorityPort = Nothing + } + ), + uriPath = "/aytamah", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + }, + _edCertAuthnResponse = certs + }, + _idpExtraInfo = + WireIdP + { _team = (either error id . parseIdFromText) "fc5f3bf8-c296-69e7-27fd-70d483740fe4", + _apiVersion = Just WireIdPAPIV1, + _oldIssuers = + [ Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "hele.johanna"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + } + ], + _replacedBy = Nothing, + _handle = IdPHandle {unIdPHandle = "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _domain = Nothing + } + } diff --git a/libs/wire-api/test/golden/testObject_IdP_1.json b/libs/wire-api/test/golden/testObject_IdP_1.json new file mode 100644 index 00000000000..6d5614ceb20 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_IdP_1.json @@ -0,0 +1,22 @@ +{ + "extraInfo": { + "apiVersion": null, + "domain": "wire.com", + "handle": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "oldIssuers": [ + "https://hele.johanna/", + "https://ulli.jannis/", + "https://reet.loviise/" + ], + "replacedBy": "fc5f3bf8-c296-69e7-27fd-70d483740fe4", + "team": "fc5f3bf8-c296-69e7-27fd-70d483740fe4" + }, + "id": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "metadata": { + "certAuthnResponse": [ + "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk" + ], + "issuer": "https://liisa.kaisa/", + "requestURI": "https://johanna.leks/aytamah" + } +} diff --git a/libs/wire-api/test/golden/testObject_IdP_2.json b/libs/wire-api/test/golden/testObject_IdP_2.json new file mode 100644 index 00000000000..e6ae1cacd05 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_IdP_2.json @@ -0,0 +1,18 @@ +{ + "extraInfo": { + "apiVersion": "WireIdPAPIV2", + "domain": null, + "handle": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "oldIssuers": [], + "replacedBy": null, + "team": "fc5f3bf8-c296-69e7-27fd-70d483740fe4" + }, + "id": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "metadata": { + "certAuthnResponse": [ + "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk" + ], + "issuer": "https://liisa.kaisa/", + "requestURI": "https://johanna.leks/aytamah" + } +} diff --git a/libs/wire-api/test/golden/testObject_IdP_3.json b/libs/wire-api/test/golden/testObject_IdP_3.json new file mode 100644 index 00000000000..e7619ad16e9 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_IdP_3.json @@ -0,0 +1,21 @@ +{ + "extraInfo": { + "apiVersion": "WireIdPAPIV1", + "domain": null, + "handle": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "oldIssuers": [ + "https://hele.johanna/" + ], + "replacedBy": null, + "team": "fc5f3bf8-c296-69e7-27fd-70d483740fe4" + }, + "id": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "metadata": { + "certAuthnResponse": [ + "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk", + "MIIDpDCCAoygAwIBAgIGAWSx7x1HMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi01MDA1MDgxHDAaBgkqhkiG9w0BCQEWDWluZm9Ab2t0YS5jb20wHhcNMTgwNzE5MDk0NTM1WhcNMjgwNzE5MDk0NjM0WjCBkjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNVBAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtNTAwNTA4MRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhUaQm/3dgPws1A5IjFK9ZQpj170vIqENuDG0tapAzkvk6+9vyhduGckHTeZF3k5MMlW9iix2Eg0qa1oS/Wrq/aBf7+BH6y1MJlQnaKQ3hPL+OFvYzbnrN8k2uC2LivP7Y90dXwtN3P63rA4QSyDPYEMvdKSubUKX/HNsUg4I2PwHmpfWBNgoMkqe0bxQILBv+84L62IYSd6k77XXnCFb/usHpG/gY6sJsTQ2aFl9FuJ51uf67AOj8RzPXstgtUaXbdJI0kAqKIb3j9Zv3mpPCy/GHnyB3PMalvtc1uaz1ZnwO2eliqhwB6/8W6CPutFo1Bhq1glQIX+1OD7906iORwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB0h6vKAywJwH3g0RnocOpBvT42QW57TZ3Wzm9gbg6dQL0rB+NHDx2V0VIh51E3YHL1os9W09MreM7I74D/fX27r1Q3+qAsL1v3CN8WIVh9eYitBCtF7DwZmL2UXTia+GWPrabO14qAztFmTXfqNuCZej7gJd/K2r0KBiZtZ6o58WBREW2F70a6nN6Nk1yjzBkDTJMMf8OMXHphTaalMBXojN9W6HEDpGBE0qY7c70PqvfUEzd8wHWcDxo6+3jajajelk0V4rg7Cqxccr+WwjYtENEuQypNG2mbI52iPZked0QWKy0WzhSMw5wjJ+QDG31vJInAB2769C2KmhPDyNhU" + ], + "issuer": "https://liisa.kaisa/", + "requestURI": "https://johanna.leks/aytamah" + } +} diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 80c4e211369..0d0a1666608 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -631,6 +631,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.FederationStatus Test.Wire.API.Golden.Manual.GetPaginatedConversationIds Test.Wire.API.Golden.Manual.GroupId + Test.Wire.API.Golden.Manual.IdP Test.Wire.API.Golden.Manual.InvitationUserView Test.Wire.API.Golden.Manual.ListConversations Test.Wire.API.Golden.Manual.ListUsersById From edd7c9794ff153e07bd7e4771e846d83e94fedd5 Mon Sep 17 00:00:00 2001 From: jschaul Date: Wed, 7 Jan 2026 13:52:21 +0100 Subject: [PATCH 47/60] fix the cleanup script to delete all leftover helm releases in test-* namespaces older than 2 hours (#4937) --- hack/bin/integration-cleanup.sh | 33 +++++++++++++-------- hack/bin/integration-teardown-federation.sh | 2 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/hack/bin/integration-cleanup.sh b/hack/bin/integration-cleanup.sh index 87eb8fddaac..de5d24a1471 100755 --- a/hack/bin/integration-cleanup.sh +++ b/hack/bin/integration-cleanup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Script to delete any helm releases prefixed with `test-` older than 2 hours deemed inactive +# Script to delete any integration namespaces prefixed with `test-` older than 2 hours. # Also deletes leftover nginx ingress classes. # # Motivation: cleanup of old test clusters that were not deleted (e.g. by the CI system, because it broke) @@ -9,18 +9,27 @@ set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -releases=$(helm list -A -f '^test-' -o json | - jq -r -f "$DIR/filter-old-releases.jq") +NOW=$(date +%s) +namespaces=$(kubectl get namespaces -o json | jq -r --argjson now "$NOW" ' + .items[] + | .metadata as $meta + | $meta.name as $name + | select($name | startswith("test-")) + | select($name | contains("-fed2") | not) + | ($meta.creationTimestamp | fromdateiso8601) as $created + | select(($now - $created) > (2 * 60 * 60)) + | $name +') -if [ -n "$releases" ]; then - while read -r line; do - name=$(awk '{print $1}' <<<"$line") - namespace=$(awk '{print $2}' <<<"$line") - echo "test release '$name' older than 2 hours; deleting..." - helm delete -n "$namespace" "$name" - done <<<"$releases" -else +if [ -z "$namespaces" ]; then echo "Nothing to clean up." +else + while read -r namespace; do + echo "Test namespace '$namespace' older than 2 hours; tearing down..." + if ! NAMESPACE="$namespace" "${DIR}/integration-teardown-federation.sh"; then + echo "Failed to tear down namespace '$namespace'; continuing..." + fi + done <<<"$namespaces" fi -"${DIR}"/integration-teardown-ingress-classes.sh +"${DIR}/integration-teardown-ingress-classes.sh" diff --git a/hack/bin/integration-teardown-federation.sh b/hack/bin/integration-teardown-federation.sh index a0074f0842d..9b97eed327e 100755 --- a/hack/bin/integration-teardown-federation.sh +++ b/hack/bin/integration-teardown-federation.sh @@ -22,4 +22,4 @@ export INGRESS_CHART="ingress-nginx-controller" . "$DIR/helm_overrides.sh" helmfile --environment "$HELMFILE_ENV" --file "${TOP_LEVEL}/hack/helmfile.yaml.gotmpl" destroy --skip-deps --skip-charts --concurrency 0 || echo "Failed to delete helm deployments, ignoring this failure as next steps will the destroy namespaces anyway." -kubectl delete namespace "$NAMESPACE_1" "$NAMESPACE_2" +kubectl delete namespace "$NAMESPACE_1" "$NAMESPACE_2" --wait=false From 0e10b7fb9a0c0951be7d6e73c4bc844f31cc8bd6 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 7 Jan 2026 14:53:31 +0100 Subject: [PATCH 48/60] Tweak cabal.project.local.tweak and force people to know about it. (#4925) --- Makefile | 10 +++++++--- hack/bin/cabal.project.local.template | 6 ------ hack/cabal.project.local.template | 12 ++++++++++++ 3 files changed, 19 insertions(+), 9 deletions(-) delete mode 100644 hack/bin/cabal.project.local.template create mode 100644 hack/cabal.project.local.template diff --git a/Makefile b/Makefile index 10cb677a951..3f5894e49bb 100644 --- a/Makefile +++ b/Makefile @@ -74,8 +74,8 @@ ifeq ("$(package)", "all") else -if ( test -e dist || test -e dist-newstyle ); then find dist* -type d -name '$(package)-*' -exec rm -rf {}; fi endif - # `/dist` shouldn't be created or used by anybody any more, we're just making sure here. - -rm -rf dist + # `/dist` and `.ghc.environment` shouldn't be created or used by anybody any more, we're just making sure here. + -rm -rf dist .ghc.environment -rm -f "bill-of-materials.$(HELM_SEMVER).json" .PHONY: clean-hint @@ -88,7 +88,7 @@ clean-hint: .PHONY: cabal.project.local cabal.project.local: - cp ./hack/bin/cabal.project.local.template ./cabal.project.local + cp ./hack/cabal.project.local.template ./cabal.project.local # Usage: make c package=brig test=1 .PHONY: c @@ -96,6 +96,10 @@ c: treefmt c-fast .PHONY: c c-fast: + if [ ! -e "cabal.project.local" ]; then \ + echo "'cabal.project.local' not found. please run 'make cabal.project.local' and tweak the output to your liking." + exit 1; \ + fi cabal build $(WIRE_CABAL_BUILD_OPTIONS) $(package) || ( make clean-hint; false ) ifeq ($(test), 1) ./hack/bin/cabal-run-tests.sh $(package) $(testargs) diff --git a/hack/bin/cabal.project.local.template b/hack/bin/cabal.project.local.template deleted file mode 100644 index 9264d3a48f4..00000000000 --- a/hack/bin/cabal.project.local.template +++ /dev/null @@ -1,6 +0,0 @@ -test-show-details: direct -profiling: False -profiling-detail: late -optimization: False -program-options - ghc-options: -O0 diff --git a/hack/cabal.project.local.template b/hack/cabal.project.local.template new file mode 100644 index 00000000000..dd27e423715 --- /dev/null +++ b/hack/cabal.project.local.template @@ -0,0 +1,12 @@ +-- disable profilling. if you are using hls/lsp, you probably want to +-- enable this. same as `ghc-options: -fwrite-ide-info`. +profiling: False +profiling-detail: late + +-- .ghc.environment is not used in .cabal-v2 and may conflict with hls. +write-ghc-environment-files: never + +-- disable optimization. very important for dev machines. implies `ghc-options: -O0`. +optimization: False + +test-show-details: direct From 52a6b7c87629c199e7a595727fa62b2f56776909 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Jan 2026 08:25:38 +0100 Subject: [PATCH 49/60] WPB-22577 [fix] Postgres migration for backendA fails on CI (#4931) --- changelog.d/5-internal/WPB-22577 | 1 + integration/test/Testlib/Run.hs | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 changelog.d/5-internal/WPB-22577 diff --git a/changelog.d/5-internal/WPB-22577 b/changelog.d/5-internal/WPB-22577 new file mode 100644 index 00000000000..145fc336176 --- /dev/null +++ b/changelog.d/5-internal/WPB-22577 @@ -0,0 +1 @@ +Fix postgres migrations on CI test runs diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 1ae1ddf06dd..0566cc00f22 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -28,7 +28,7 @@ import Data.Foldable import Data.Function import Data.Functor import Data.List -import Data.Maybe (fromMaybe) +import Data.Maybe import Data.String (IsString (fromString)) import Data.String.Conversions (cs) import Data.Text (Text) @@ -188,10 +188,12 @@ runMigrations :: App () runMigrations = do cwdBase <- asks (.servicesCwdBase) let brig = "brig" - let (cwd, exe) = case cwdBase of + (cwd, exe) = case cwdBase of Nothing -> (Nothing, brig) Just dir -> (Just (dir brig), "../../dist" brig) + -- servicesCwdBase is only set for local binaries + isLocal = isJust cwd getConfig <- readAndUpdateConfig def backendA Brig config <- liftIO getConfig tempFile <- liftIO $ writeTempFile "/tmp" "brig-migrations.yaml" (cs $ Yaml.encode config) @@ -199,7 +201,7 @@ runMigrations = do pool <- asks (.resourcePool) lowerCodensity $ do resources <- acquireResources (length dynDomains) pool - let dbnames = [backendA.berPostgresqlDBName, backendB.berPostgresqlDBName] <> map (.berPostgresqlDBName) resources + let dbnames = [dbs | isLocal, dbs <- [backendA.berPostgresqlDBName, backendB.berPostgresqlDBName]] <> map (.berPostgresqlDBName) resources for_ dbnames $ runMigration exe tempFile cwd liftIO $ putStrLn "Postgres migrations finished" where From b7a7f7d8eae7ecfee68d599c67ab005ba7e6da8a Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 8 Jan 2026 08:25:56 +0100 Subject: [PATCH 50/60] WPB-9391 Haddocks comments on legalhold checks (#4934) --- services/brig/src/Brig/API/Connection.hs | 5 ++++- services/galley/src/Galley/API/Action.hs | 8 ++++++++ services/galley/src/Galley/API/LegalHold/Conflicts.hs | 4 ++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index b6426f86243..63a9526e42f 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -194,7 +194,10 @@ createConnectionToLocalUser self conn target = do change :: UserConnection -> RelationWithHistory -> ExceptT ConnectionError (AppT r) (ResponseForExistedCreated UserConnection) change c s = Existed <$> lift (wrapClient $ Data.updateConnection c s) --- | Throw error if one user has a LH device and the other status `no_consent` or vice versa. +-- | Guard local connection creation against legal-hold consent conflicts. +-- Rejects when one user is `no_consent` while the other has LH enabled. +-- See also: "Galley.API.LegalHold.Conflicts.guardLegalholdPolicyConflictsUid" +-- and "Galley.API.Action.checkLHPolicyConflictsLocal". -- -- FUTUREWORK: we may want to move this to the LH application logic, so we can recycle it for -- group conv creation and possibly other situations. diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index e87d8eea908..a29eb18d893 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -695,6 +695,14 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do throw FederationNotConfigured ensureConnectedToRemotes lusr remotes + -- \| Guard conversation member additions against legal-hold consent conflicts: + -- - if any conv member has LH enabled then all new users must give consent + -- - if any new user has LH enabled then all new users must give consent + -- - if new users have LH enabled then + -- - ensure that a consented conv admin exists + -- - and kick all existing members that do not consent to LH from the conversation + -- See also: "Brig.API.Connection.checkLegalholdPolicyConflict" + -- and "Galley.API.LegalHold.Conflicts.guardLegalholdPolicyConflictsUid". checkLHPolicyConflictsLocal :: [UserId] -> Sem r () diff --git a/services/galley/src/Galley/API/LegalHold/Conflicts.hs b/services/galley/src/Galley/API/LegalHold/Conflicts.hs index a073f055f29..705d3b2f28a 100644 --- a/services/galley/src/Galley/API/LegalHold/Conflicts.hs +++ b/services/galley/src/Galley/API/LegalHold/Conflicts.hs @@ -105,6 +105,10 @@ guardLegalholdPolicyConflicts (ProtectedUser self) otherClients = do FeatureLegalHoldDisabledByDefault -> guardLegalholdPolicyConflictsUid self otherClients FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> guardLegalholdPolicyConflictsUid self otherClients +-- | Guard notification handling against legal-hold policy conflicts. +-- Ensures that if any user has a LH client then no user can be missing consent. +-- See also: "Brig.API.Connection.checkLegalholdPolicyConflict" +-- and "Galley.API.Action.checkLHPolicyConflictsLocal". guardLegalholdPolicyConflictsUid :: forall r. ( Member BrigAPIAccess r, From 882e66fa9d8de2e2b515b0b11b4f9199ba37bb9e Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 8 Jan 2026 09:35:00 +0100 Subject: [PATCH 51/60] Use nix flakes instead of niv and manually pinned git dependencies (#4933) * Initailize flake * Remove niv stuff * flake: Add nixpkgs 24.11 for cabal 3.12 * flake: Keep the same rev for nixpkgs, so other problems can be tackled later * Use the flake in all scripts * ciImage: Enable flakes * nix: Expose explicit derivation that allows building all images at once * nix: Use flake inputs to pin haskell dependencies * flake.nix: Use branch names instead of revs for haskell pins Also use published versions of warp and http2, they already contain the changes that were pinned * hack: Remove the need to build wireServer.imageList the images.all derivation builds a nice link farm which can be used instead. --- .envrc | 4 +- Makefile | 9 +- changelog.d/5-internal/flake | 1 + docs/src/developer/developer/building.md | 12 +- flake.lock | 366 +++++++++++++++++++++++ flake.nix | 113 +++++++ hack/bin/kind-upload-image.sh | 17 +- hack/bin/kind-upload-images.sh | 17 +- hack/bin/nix-hls.sh | 2 +- hack/bin/upload-image.sh | 18 +- hack/bin/upload-images.sh | 22 +- nix/default.nix | 16 +- nix/haskell-pins.nix | 173 +++-------- nix/manual-overrides.nix | 3 + nix/sources.json | 26 -- nix/sources.nix | 198 ------------ nix/wire-server.nix | 90 +++--- treefmt.toml | 3 - 18 files changed, 620 insertions(+), 470 deletions(-) create mode 100644 changelog.d/5-internal/flake create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 nix/sources.json delete mode 100644 nix/sources.nix diff --git a/.envrc b/.envrc index 675fb0829f7..ccf83c95b43 100644 --- a/.envrc +++ b/.envrc @@ -15,7 +15,7 @@ store_paths=$(echo "$nix_files" ./services/nginz/third_party/nginx-zauth-module/ layout_dir=$(direnv_layout_dir) env_dir=./.env -export NIX_CONFIG='extra-experimental-features = nix-command' +export NIX_CONFIG='extra-experimental-features = nix-command flakes' [[ -d "$layout_dir" ]] || mkdir -p "$layout_dir" @@ -27,7 +27,7 @@ if [[ ! -d "$env_dir" || ! -f "$layout_dir/nix-rebuild" || "$store_paths" != $(< fi fi echo "🔧 Building environment" - $bcmd build -f nix wireServer.devEnv -Lv --out-link ./.env --fallback + $bcmd build '.#wireServer.devEnv' -Lv --out-link ./.env --fallback echo "$store_paths" >"$layout_dir/nix-rebuild" fi diff --git a/Makefile b/Makefile index 3f5894e49bb..48f19ae18e0 100644 --- a/Makefile +++ b/Makefile @@ -307,7 +307,7 @@ treefmt-check: .PHONY: build-image-% build-image-%: - nix-build ./nix -A wireServer.imagesNoDocs.$(*) && \ + nix build '.#wireServer.imagesNoDocs.$(*)' && \ ./result | docker load | tee /tmp/imageName-$(*) && \ imageName=$$(grep quay.io /tmp/imageName-$(*) | awk '{print $$3}') && \ echo 'You can run your image locally using' && \ @@ -323,8 +323,11 @@ upload-images: upload-images-dev: ./hack/bin/upload-images.sh imagesUnoptimizedNoDocs +HOOGLE_IMAGE_DIR := $(shell mktemp -d -t wire-server-hoogle-image.XXXXXX) + upload-hoogle-image: - ./hack/bin/upload-image.sh wireServer.hoogleImage + nix -v --show-trace -L build ".#wireServer.hoogleImage" --out-link $(HOOGLE_IMAGE_DIR)/image --fallback + ./hack/bin/upload-image.sh $(HOOGLE_IMAGE_DIR)/image ################################# ## cassandra / postgres management @@ -669,7 +672,7 @@ helm-template-%: clean-charts charts-integration ./hack/bin/helm-template.sh $(*) sbom.json: - nix -Lv build -f nix wireServer.bomDependencies && \ + nix -Lv build '.#wireServer.bomDependencies' && \ nix run 'github:wireapp/tom-bombadil#create-sbom' -- --root-package-name "wire-server" # Ask the security team for the `DEPENDENCY_TRACK_API_KEY` (if you need it) diff --git a/changelog.d/5-internal/flake b/changelog.d/5-internal/flake new file mode 100644 index 00000000000..aaba7fcf9f8 --- /dev/null +++ b/changelog.d/5-internal/flake @@ -0,0 +1 @@ +Use nix flakes instead of niv and manually pinned git dependencies \ No newline at end of file diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index dd9ecc2ee84..6b754e073bb 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -99,24 +99,24 @@ you may build each individual service by running ```bash nix build -Lv \ - --experimental-features 'nix-command' \ - -f ./nix wireServer. + --experimental-features 'nix-command flakes' \ + '.#wireServer.' ``` you may build all the libraries that exist locally or are in the closure of `wire-server` by running ```bash nix build -Lv \ - --experimental-features 'nix-command' \ - -f ./nix wireServer.haskellPackages. + --experimental-features 'nix-command flakes' \ + '.#wireServer.haskellPackages.' ``` you may build all the images that would be deployed by running ```bash nix build -Lv \ - --experimental-features 'nix-command' \ - -f ./nix wireServer.allImages + --experimental-features 'nix-command flakes' \ + '.#wireServer.allImages' ``` > ℹ️ Info diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000000..19154d022be --- /dev/null +++ b/flake.lock @@ -0,0 +1,366 @@ +{ + "nodes": { + "amazonka": { + "flake": false, + "locked": { + "lastModified": 1759730860, + "narHash": "sha256-cCRhHH/IgM7tPy8rXHTSRec1zxohO8NWxSVZEG1OjQw=", + "owner": "brendanhay", + "repo": "amazonka", + "rev": "a7d699be1076e2aad05a1930ca3937ffea954ad8", + "type": "github" + }, + "original": { + "owner": "brendanhay", + "repo": "amazonka", + "rev": "a7d699be1076e2aad05a1930ca3937ffea954ad8", + "type": "github" + } + }, + "bloodhound": { + "flake": false, + "locked": { + "lastModified": 1739958389, + "narHash": "sha256-E3co9FGZP135T3RocX4vbUELbbgGbYddD8CcVNUzHu8=", + "owner": "wireapp", + "repo": "bloodhound", + "rev": "dac0f1384b335ce35dc026bf8154e574b1a15d62", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "wire-fork", + "repo": "bloodhound", + "type": "github" + } + }, + "cql": { + "flake": false, + "locked": { + "lastModified": 1693567589, + "narHash": "sha256-2MYwZKiTdwgjJdLNvECi7gtcIo+3H4z1nYzen5x0lgU=", + "owner": "wireapp", + "repo": "cql", + "rev": "abbd2739969d17a909800f282d10d42a254c4e3b", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "develop", + "repo": "cql", + "type": "github" + } + }, + "cql-io": { + "flake": false, + "locked": { + "lastModified": 1661159563, + "narHash": "sha256-DMRWUq4yorG5QFw2ZyF/DWnRjfnzGupx0njTiOyLzPI=", + "owner": "wireapp", + "repo": "cql-io", + "rev": "c2b6aa995b5817ed7c78c53f72d5aa586ef87c36", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "control-conn", + "repo": "cql-io", + "type": "github" + } + }, + "cryptobox-haskell": { + "flake": false, + "locked": { + "lastModified": 1728557781, + "narHash": "sha256-LROqEzzvKiJ7YoF8SdKUkEgGXKBRW6Wdtd4EBY3LYOk=", + "owner": "wireapp", + "repo": "cryptobox-haskell", + "rev": "05560b2cfae13aac54414952638dadd62204f361", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "master", + "repo": "cryptobox-haskell", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "hedis": { + "flake": false, + "locked": { + "lastModified": 1748594228, + "narHash": "sha256-BwcqQZf2GaEn2i6o9bVl+jiu/CjShYlHCmO81bYfc8Y=", + "owner": "wireapp", + "repo": "hedis", + "rev": "00d7fbf5f19b812b9e64e12be8860c4741be8558", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "wire-changes", + "repo": "hedis", + "type": "github" + } + }, + "hsaml2": { + "flake": false, + "locked": { + "lastModified": 1717163391, + "narHash": "sha256-gufEAC7fFqafG8dXkGIOSfAcVv+ZWkawmBgUV+Ics2s=", + "owner": "dylex", + "repo": "hsaml2", + "rev": "874627ad22e69afe4d9a797e39633ffb30697c78", + "type": "github" + }, + "original": { + "owner": "dylex", + "ref": "main", + "repo": "hsaml2", + "type": "github" + } + }, + "hspec-wai": { + "flake": false, + "locked": { + "lastModified": 1699866697, + "narHash": "sha256-Nc5POjA+mJt7Vi3drczEivGsv9PXeVOCSwp21lLmz58=", + "owner": "wireapp", + "repo": "hspec-wai", + "rev": "08176f07fa893922e2e78dcaf996c33d79d23ce2", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "body-contains", + "repo": "hspec-wai", + "type": "github" + } + }, + "http-client": { + "flake": false, + "locked": { + "lastModified": 1706706086, + "narHash": "sha256-z47GlT+tHsSlRX4ApSGQIpOpaZiBeqr72/tWuvzw8tc=", + "owner": "wireapp", + "repo": "http-client", + "rev": "37494bb9a89dd52f97a8dc582746c6ff52943934", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "master", + "repo": "http-client", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1765772535, + "narHash": "sha256-aq+dQoaPONOSjtFIBnAXseDm9TUhIbe215TPmkfMYww=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "09b8fda8959d761445f12b55f380d90375a1d6bb", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "09b8fda8959d761445f12b55f380d90375a1d6bb", + "type": "github" + } + }, + "nixpkgs_24_11": { + "locked": { + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "postie": { + "flake": false, + "locked": { + "lastModified": 1755365380, + "narHash": "sha256-gSWoV2EuqxTiVJgG5DBvpR2GmccAD/tRdGVxoNw8+Rw=", + "owner": "alexbiehl", + "repo": "postie", + "rev": "769dde424327c6b83079d79130a3d476967a9790", + "type": "github" + }, + "original": { + "owner": "alexbiehl", + "ref": "master", + "repo": "postie", + "type": "github" + } + }, + "root": { + "inputs": { + "amazonka": "amazonka", + "bloodhound": "bloodhound", + "cql": "cql", + "cql-io": "cql-io", + "cryptobox-haskell": "cryptobox-haskell", + "flake-utils": "flake-utils", + "hedis": "hedis", + "hsaml2": "hsaml2", + "hspec-wai": "hspec-wai", + "http-client": "http-client", + "nixpkgs": "nixpkgs", + "nixpkgs_24_11": "nixpkgs_24_11", + "postie": "postie", + "servant-openapi3": "servant-openapi3", + "tasty": "tasty", + "tasty-ant-xml": "tasty-ant-xml", + "text-icu-translit": "text-icu-translit", + "tinylog": "tinylog", + "wai-predicates": "wai-predicates" + } + }, + "servant-openapi3": { + "flake": false, + "locked": { + "lastModified": 1716983629, + "narHash": "sha256-iKMWd+qm8hHhKepa13VWXDPCpTMXxoOwWyoCk4lLlIY=", + "owner": "wireapp", + "repo": "servant-openapi3", + "rev": "0db0095040df2c469a48f5b8724595f82afbad0c", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "required-request-bodies", + "repo": "servant-openapi3", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "tasty": { + "flake": false, + "locked": { + "lastModified": 1705586441, + "narHash": "sha256-oACehxazeKgRr993gASRbQMf74heh5g0B+70ceAg17I=", + "owner": "wireapp", + "repo": "tasty", + "rev": "97df5c1db305b626ffa0b80055361b7b28e69cec", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "mangoiv/full-stacktrace-rebased", + "repo": "tasty", + "type": "github" + } + }, + "tasty-ant-xml": { + "flake": false, + "locked": { + "lastModified": 1746711397, + "narHash": "sha256-Aj/iTVECsCGq4f+32FXWyYj/iLH5e4Gm4hYRmewnJJM=", + "owner": "wireapp", + "repo": "tasty-ant-xml", + "rev": "11c53e976e2e941f25a33e8768669eb576d19ea8", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "drop-console-formatting_rebased", + "repo": "tasty-ant-xml", + "type": "github" + } + }, + "text-icu-translit": { + "flake": false, + "locked": { + "lastModified": 1732177438, + "narHash": "sha256-wOZMz0yv29WgQyUuJ8fDejR11GopAUWkeh3nV0zlrow=", + "owner": "wireapp", + "repo": "text-icu-translit", + "rev": "2392d8d1500cd16e12aede1e0a3863ad3c1a7e37", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "master", + "repo": "text-icu-translit", + "type": "github" + } + }, + "tinylog": { + "flake": false, + "locked": { + "lastModified": 1674551828, + "narHash": "sha256-htEIJY+LmIMACVZrflU60+X42/g14NxUyFM7VJs4E6w=", + "owner": "wireapp", + "repo": "tinylog", + "rev": "9609104263e8cd2a631417c1c3ef23e090de0d09", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "wire-fork", + "repo": "tinylog", + "type": "github" + } + }, + "wai-predicates": { + "flake": false, + "locked": { + "lastModified": 1732803463, + "narHash": "sha256-+v3nGZhW/pIki2/ax4sMLeR2F6Ikh7V1/JbGJnZC3Pc=", + "owner": "wireapp", + "repo": "wai-predicates", + "rev": "35b0ac568b5e197b21acc12699ed09ee89c1d994", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "develop", + "repo": "wai-predicates", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000000..6eb24cb1665 --- /dev/null +++ b/flake.nix @@ -0,0 +1,113 @@ +{ + description = "A very basic flake"; + + inputs = { + self.submodules = true; + nixpkgs.url = "github:nixos/nixpkgs?rev=09b8fda8959d761445f12b55f380d90375a1d6bb"; + nixpkgs_24_11.url = "github:nixos/nixpkgs?ref=nixos-24.11"; + flake-utils.url = "github:numtide/flake-utils"; + + cryptobox-haskell = { + url = "github:wireapp/cryptobox-haskell?ref=master"; + flake = false; + }; + bloodhound = { + url = "github:wireapp/bloodhound?ref=wire-fork"; + flake = false; + }; + hsaml2 = { + url = "github:dylex/hsaml2?ref=main"; + flake = false; + }; + hedis = { + url = "github:wireapp/hedis?ref=wire-changes"; + flake = false; + }; + + http-client = { + url = "github:wireapp/http-client?ref=master"; + flake = false; + }; + + hspec-wai = { + url = "github:wireapp/hspec-wai?ref=body-contains"; + flake = false; + }; + + cql = { + url = "github:wireapp/cql?ref=develop"; + flake = false; + }; + + cql-io = { + url = "github:wireapp/cql-io?ref=control-conn"; + flake = false; + }; + + wai-predicates = { + url = "github:wireapp/wai-predicates?ref=develop"; + flake = false; + }; + + tasty = { + url = "github:wireapp/tasty?ref=mangoiv/full-stacktrace-rebased"; + flake = false; + }; + + servant-openapi3 = { + url = "github:wireapp/servant-openapi3?ref=required-request-bodies"; + flake = false; + }; + + postie = { + url = "github:alexbiehl/postie?ref=master"; + flake = false; + }; + + tinylog = { + url = "github:wireapp/tinylog?ref=wire-fork"; + flake = false; + }; + + tasty-ant-xml = { + url = "github:wireapp/tasty-ant-xml?ref=drop-console-formatting_rebased"; + flake = false; + }; + + text-icu-translit = { + url = "github:wireapp/text-icu-translit?ref=master"; + flake = false; + }; + + amazonka = { + url = "github:brendanhay/amazonka?rev=a7d699be1076e2aad05a1930ca3937ffea954ad8"; + flake = false; + }; + }; + + outputs = inputs@{ nixpkgs, nixpkgs_24_11, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ + (import ./nix/overlay.nix) + (import ./nix/overlay-docs.nix) + ]; + }; + pkgs_24_11 = import nixpkgs_24_11 { + inherit system; + }; + wireServerPkgs = import ./nix { inherit pkgs pkgs_24_11 inputs; }; + in + { + # profileEnv wireServer docs docsEnv mls-test-cli nginz; + packages = { + inherit (wireServerPkgs) pkgs profileEnv wireServer docs docsEnv mls-test-cli nginz; + }; + devShells = { + default = wireServerPkgs.wireServer.devEnv; + }; + } + ); +} diff --git a/hack/bin/kind-upload-image.sh b/hack/bin/kind-upload-image.sh index 61b24c7937f..d376765f0c9 100755 --- a/hack/bin/kind-upload-image.sh +++ b/hack/bin/kind-upload-image.sh @@ -1,20 +1,15 @@ #!/usr/bin/env bash -# This script builds all the images in wireServer.images attribute of -# $ROOT_DIR/nix/default.nix and uploads them to the docker registry using the -# repository name specified in the image derivation and tag specified by -# environment variable "$DOCKER_TAG". -# -# If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to -# upload the images. -# -# This script is intended to be run by CI/CD pipelines. +# This script builds all the images in wireServer.images attribute of the flake +# and loads them into the docker daemon of kind using the repository name +# specified in the image derivation and tag specified by environment variable +# "$DOCKER_TAG". set -euo pipefail set -x -# nix attribute under wireServer from "$ROOT_DIR/nix" containing all the images +# nix attribute under wireServer containing all the images readonly IMAGE_ATTR=${1:?$usage} SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) @@ -24,7 +19,7 @@ readonly SCRIPT_DIR ROOT_DIR tmp_link_store=$(mktemp -d) image_stream_file="$tmp_link_store/image-stream" -nix -v --show-trace -L build -f "$ROOT_DIR/nix" "$IMAGE_ATTR" -o "$image_stream_file" +nix -v --show-trace -L build "$ROOT_DIR#$IMAGE_ATTR" -o "$image_stream_file" image_file="$tmp_link_store/image" image_file_tagged="$tmp_link_store/image-tagged" "$image_stream_file" > "$image_file" diff --git a/hack/bin/kind-upload-images.sh b/hack/bin/kind-upload-images.sh index b1fea5cf980..cf97a44b839 100755 --- a/hack/bin/kind-upload-images.sh +++ b/hack/bin/kind-upload-images.sh @@ -1,20 +1,15 @@ #!/usr/bin/env bash -# This script builds all the images in wireServer.images attribute of -# $ROOT_DIR/nix/default.nix and uploads them to the docker registry using the -# repository name specified in the image derivation and tag specified by -# environment variable "$DOCKER_TAG". -# -# If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to -# upload the images. -# -# This script is intended to be run by CI/CD pipelines. +# This script builds all the images in wireServer.images attribute of the flake +# and loads into the docker daemon of kind using the repository name specified +# in the image derivation and tag specified by environment variable +# "$DOCKER_TAG". set -euo pipefail set -x -# nix attribute under wireServer from "$ROOT_DIR/nix" containing all the images +# nix attribute under wireServer containing all the images readonly IMAGES_ATTR="imagesUnoptimizedNoDocs" SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) @@ -23,7 +18,7 @@ readonly SCRIPT_DIR ROOT_DIR tmp_link_store=$(mktemp -d) image_list_file="$tmp_link_store/image-list" -nix -v --show-trace -L build -f "$ROOT_DIR/nix" wireServer.imagesList -o "$image_list_file" +nix -v --show-trace -L build "$ROOT_DIR#wireServer.imagesList" -o "$image_list_file" xargs -I {} -P 10 "$SCRIPT_DIR/kind-upload-image.sh" "wireServer.$IMAGES_ATTR.{}" < "$image_list_file" diff --git a/hack/bin/nix-hls.sh b/hack/bin/nix-hls.sh index 5b66546ee50..827ad240b2f 100755 --- a/hack/bin/nix-hls.sh +++ b/hack/bin/nix-hls.sh @@ -5,7 +5,7 @@ set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TOP_LEVEL="$(cd "$DIR/../.." && pwd)" -direnv="$(nix-build --no-out-link "$TOP_LEVEL/nix" -A pkgs.direnv)/bin/direnv" +direnv="$(nix build --no-link --print-out-paths "$TOP_LEVEL#pkgs.direnv")/bin/direnv" # shellcheck disable=SC2016 maxMemory=$("$direnv" exec "$TOP_LEVEL" bash -c 'echo "$HLS_MAX_MEMORY"') diff --git a/hack/bin/upload-image.sh b/hack/bin/upload-image.sh index a070b8661bb..080c18d8dcc 100755 --- a/hack/bin/upload-image.sh +++ b/hack/bin/upload-image.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # This script builds an from the attribute provided at $1, which must be present -# in $ROOT_DIR/nix/default.nix, and uploads it to the docker registry using the -# repository name specified in the image derivation and tag specified by -# environment variable "$DOCKER_TAG". +# in the flake, and uploads it to the docker registry using the repository name +# specified in the image derivation and tag specified by environment variable +# "$DOCKER_TAG". # # If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to # upload the images. @@ -14,12 +14,8 @@ set -euo pipefail readonly DOCKER_TAG=${DOCKER_TAG:?"Please set the DOCKER_TAG env variable"} -readonly usage="USAGE: $0 " -readonly IMAGE_ATTR=${1:?$usage} - -SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) -ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &>/dev/null && pwd) -readonly SCRIPT_DIR ROOT_DIR +readonly usage="USAGE: $0 " +readonly IMAGE_STREAM_FILE=${1:?$usage} credsArgs="" if [[ "${DOCKER_USER+x}" != "" ]]; then @@ -63,10 +59,8 @@ tmp_link_store=$(mktemp -d) # product of other store paths which should already be cached and a lot of our # images should have a few common layers. More information: # https://nixos.org/manual/nixpkgs/unstable/#ssec-pkgs-dockerTools-streamLayeredImage -image_stream_file="$tmp_link_store/image_stream" -nix -v --show-trace -L build -f "$ROOT_DIR/nix" "$IMAGE_ATTR" -o "$image_stream_file" image_file="$tmp_link_store/image" -"$image_stream_file" >"$image_file" +"$IMAGE_STREAM_FILE" >"$image_file" repo=$(skopeo list-tags "docker-archive://$image_file" | jq -r '.Tags[0] | split(":") | .[0]') printf "*** Uploading $image_file to %s:%s\n" "$repo" "$DOCKER_TAG" # shellcheck disable=SC2086 diff --git a/hack/bin/upload-images.sh b/hack/bin/upload-images.sh index 89c0b721c72..9ccf1ad1874 100755 --- a/hack/bin/upload-images.sh +++ b/hack/bin/upload-images.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash -# This script builds all the images in wireServer.images attribute of -# $ROOT_DIR/nix/default.nix and uploads them to the docker registry using the -# repository name specified in the image derivation and tag specified by -# environment variable "$DOCKER_TAG". +# This script builds all the images in wireServer.images attribute of the flake +# and uploads them to the docker registry using the repository name specified in +# the image derivation and tag specified by environment variable "$DOCKER_TAG". # # If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to # upload the images. @@ -14,21 +13,20 @@ set -euo pipefail readonly usage="USAGE: $0 " -# nix attribute under wireServer from "$ROOT_DIR/nix" containing all the images +# nix attribute under wireServer containing all the images readonly IMAGES_ATTR=${1:?$usage} SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &>/dev/null && pwd) readonly SCRIPT_DIR ROOT_DIR -tmp_link_store=$(mktemp -d) -image_list_file="$tmp_link_store/image-list" -nix -v --show-trace -L build -f "$ROOT_DIR/nix" wireServer.imagesList -o "$image_list_file" --fallback - # Build everything first so we can benefit the most from having many cores. -nix -v --show-trace -L build -f "$ROOT_DIR/nix" "wireServer.$IMAGES_ATTR" --no-link --fallback +result=$(mktemp -d -t stream-images.XXXXXX) +nix -v --show-trace -L build "$ROOT_DIR#wireServer.$IMAGES_ATTR.all" --out-link "$result/images" --fallback -xargs -I {} -P 10 "$SCRIPT_DIR/upload-image.sh" "wireServer.$IMAGES_ATTR.{}" < "$image_list_file" +find "$result/images/" -type l -print0 | xargs -0 -I {} -P 10 "$SCRIPT_DIR/upload-image.sh" {} printf '*** Uploading image %s\n' nginz -"$SCRIPT_DIR/upload-image.sh" nginz +nginz_image=$(mktemp -d -t stream-nginz-image.XXXXXX) +nix -v --show-trace -L build "$ROOT_DIR#nginz" --out-link "$nginz_image/image" --fallback +"$SCRIPT_DIR/upload-image.sh" "$nginz_image/image" diff --git a/nix/default.nix b/nix/default.nix index 4d731d0573d..159199845c5 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,17 +1,5 @@ +{ pkgs, pkgs_24_11, inputs }: let - sources = import ./sources.nix; - - pkgs = import sources.nixpkgs { - config.allowUnfree = true; - overlays = [ - # All wire-server specific packages - (import ./overlay.nix) - (import ./overlay-docs.nix) - ]; - }; - - pkgs_24_11 = import sources.nixpkgs_24_11 { }; - profileEnv = pkgs.writeTextFile { name = "profile-env"; destination = "/.profile"; @@ -22,7 +10,7 @@ let ''; }; - wireServer = import ./wire-server.nix pkgs pkgs_24_11; + wireServer = import ./wire-server.nix pkgs pkgs_24_11 inputs; nginz = pkgs.callPackage ./nginz.nix { }; # packages necessary to build wire-server docs diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 273ba19bb32..5481009262b 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -1,23 +1,20 @@ # How to add a git pin: # -# 1. If your target git repository has only package with the cabal file at the +# 1. Add the target git repo to the inputs section of flake.nix like this: +# = { +# url = "github:/?rev="; +# flake = false; +# }; +# 2. If your target git repository has only package with the cabal file at the # root, add it like this under 'gitPins': # = { -# src = fetchgit { -# url = ""; -# rev = ""; -# sha256 = ""; -# }; +# src = inputs.; # }; # -# 2. If your target git repsitory has many packages, add it like this under 'gitPins': +# 3. If your target git repsitory has many packages, add it like this under 'gitPins': # # = { -# src = fetchgit { -# url = ""; -# rev = ""; -# sha256 = ""; -# }; +# src = inputs.; # packages = { # = ""; # = ""; @@ -25,38 +22,30 @@ # }; # }; # -# 3. Run 'nix build -f ./nix wireServer.haskellPackagesUnoptimizedNoDocs.'. -# This should produce an error saying expected sha and the actual sha. Replace the empty string in 'sha256' with the actual -# sha. -# # How to update a git pin: # # 1. Determine the new commit ID/SHA of the git repository that you want to pin -# and update the 'rev' field of the pin under 'gitPins'. -# -# 2. Update 'sha256' field under `fetchgit` to be an empty string. (This step is optional: -# since the sha256 has changed, the error will be the same if you remove it or if you leave the -# old value in place.) -# -# 3. Run step 3. from how to add a git pin. +# and update the 'rev' param in the URL in the inputs section of the flake.nix. # # How to add a hackage pin: # # 1. Add your package like this, under 'hackagePins': # = { # version = ""; -# sha256 = "sha256-gD9b9AXpLkpPSAeg8oPBU7tsHtSNQjxIZKBo+7+r3+c="; +# sha256 = ""; # }; # -# 2. Run step 3. from how to add a git pin. +# 2. Run 'nix build '.#wireServer.haskellPackagesUnoptimizedNoDocs.'. +# This should produce an error saying expected sha and the actual sha. Replace the empty string in 'sha256' with the actual +# sha. # # How to update a hackage pin: # # 1. Update version number. # 2. Make the 'sha256' blank string. -# 3. Run step 3. from how to add a git pin. -{ lib, fetchgit, pkgs }: hself: hsuper: +# 3. Run step 2. from how to add a hackage pin. +{ lib, inputs }: hself: hsuper: let gitPins = { # ---------------- @@ -64,11 +53,7 @@ let # ---------------- cryptobox-haskell = { - src = fetchgit { - url = "https://github.com/wireapp/cryptobox-haskell"; - rev = "7546a1a25635ef65183e3d44c1052285e8401608"; - hash = "sha256-9mMVgmMB1NWCPm/3inLeF4Ouiju0uIb/92UENoP88TU="; - }; + src = inputs.cryptobox-haskell; }; # -------------------- @@ -76,40 +61,24 @@ let # -------------------- bloodhound = { - src = fetchgit { - url = "https://github.com/wireapp/bloodhound"; - rev = "dac0f1384b335ce35dc026bf8154e574b1a15d62"; - hash = "sha256-E3co9FGZP135T3RocX4vbUELbbgGbYddD8CcVNUzHu8="; - }; + src = inputs.bloodhound; }; # Merged PR https://github.com/dylex/hsaml2/pull/20 hsaml2 = { - src = fetchgit { - url = "https://github.com/dylex/hsaml2"; - rev = "874627ad22e69afe4d9a797e39633ffb30697c78"; - hash = "sha256-gufEAC7fFqafG8dXkGIOSfAcVv+ZWkawmBgUV+Ics2s="; - }; + src = inputs.hsaml2; }; # PR: https://github.com/informatikr/hedis/pull/224 # PR: https://github.com/informatikr/hedis/pull/226 # PR: https://github.com/informatikr/hedis/pull/227 hedis = { - src = fetchgit { - url = "https://github.com/wireapp/hedis"; - rev = "00d7fbf5f19b812b9e64e12be8860c4741be8558"; - sha256 = "sha256-BwcqQZf2GaEn2i6o9bVl+jiu/CjShYlHCmO81bYfc8Y="; - }; + src = inputs.hedis; }; # Our fork because we need to a few special things http-client = { - src = fetchgit { - url = "https://github.com/wireapp/http-client"; - rev = "37494bb9a89dd52f97a8dc582746c6ff52943934"; - hash = "sha256-z47GlT+tHsSlRX4ApSGQIpOpaZiBeqr72/tWuvzw8tc="; - }; + src = inputs.http-client; packages = { "http-client" = "http-client"; "http-client-tls" = "http-client-tls"; @@ -120,50 +89,30 @@ let # PR: https://github.com/hspec/hspec-wai/pull/49 hspec-wai = { - src = fetchgit { - url = "https://github.com/wireapp/hspec-wai"; - rev = "08176f07fa893922e2e78dcaf996c33d79d23ce2"; - hash = "sha256-Nc5POjA+mJt7Vi3drczEivGsv9PXeVOCSwp21lLmz58="; - }; + src = inputs.hspec-wai; }; # PR: https://gitlab.com/twittner/cql/-/merge_requests/11 cql = { - src = fetchgit { - url = "https://github.com/wireapp/cql"; - rev = "abbd2739969d17a909800f282d10d42a254c4e3b"; - hash = "sha256-2MYwZKiTdwgjJdLNvECi7gtcIo+3H4z1nYzen5x0lgU="; - }; + src = inputs.cql; }; # PR: https://gitlab.com/twittner/cql-io/-/merge_requests/20 cql-io = { - src = fetchgit { - url = "https://github.com/wireapp/cql-io"; - rev = "c2b6aa995b5817ed7c78c53f72d5aa586ef87c36"; - hash = "sha256-DMRWUq4yorG5QFw2ZyF/DWnRjfnzGupx0njTiOyLzPI="; - }; + src = inputs.cql-io; }; # missing upstream PR, this will get removed when completing # servantification # - # this is currently still used/needed in the proxy service + # this is currently still used/needed in the proxy service wai-predicates = { - src = fetchgit { - url = "https://github.com/wireapp/wai-predicates"; - rev = "ff95282a982ab45cced70656475eaf2cefaa26ea"; - hash = "sha256-x2XSv2+/+DG9FXN8hfUWGNIO7V4iBhlzYz19WWKaLKQ="; - }; + src = inputs.wai-predicates; }; # PR: https://github.com/UnkindPartition/tasty/pull/351 tasty = { - src = fetchgit { - url = "https://github.com/wireapp/tasty"; - rev = "97df5c1db305b626ffa0b80055361b7b28e69cec"; - hash = "sha256-oACehxazeKgRr993gASRbQMf74heh5g0B+70ceAg17I="; - }; + src = inputs.tasty; packages = { tasty-hunit = "hunit"; }; @@ -172,68 +121,27 @@ let # sets the required flag for HTTP request bodies. # PR: https://github.com/biocad/servant-openapi3/pull/49 servant-openapi3 = { - src = fetchgit { - url = "https://github.com/wireapp/servant-openapi3"; - rev = "0db0095040df2c469a48f5b8724595f82afbad0c"; - hash = "sha256-iKMWd+qm8hHhKepa13VWXDPCpTMXxoOwWyoCk4lLlIY="; - }; + src = inputs.servant-openapi3; }; # we need HEAD, the latest release is too old postie = { - src = fetchgit { - url = "https://github.com/alexbiehl/postie"; - rev = "13404b8cb7164cd9010c9be6cda5423194dd0c06"; - hash = "sha256-nNivtyBpr4DFsbaXxlCznX+MYtzNshU7vfVpnhMh52c="; - }; + src = inputs.postie; }; tinylog = { - src = fetchgit { - url = "https://github.com/wireapp/tinylog.git"; - rev = "9609104263e8cd2a631417c1c3ef23e090de0d09"; - hash = "sha256-htEIJY+LmIMACVZrflU60+X42/g14NxUyFM7VJs4E6w="; - }; + src = inputs.tinylog; }; # PR: https://github.com/ocharles/tasty-ant-xml/pull/32 tasty-ant-xml = { - src = fetchgit { - url = "https://github.com/wireapp/tasty-ant-xml"; - rev = "11c53e976e2e941f25a33e8768669eb576d19ea8"; - hash = "sha256-Aj/iTVECsCGq4f+32FXWyYj/iLH5e4Gm4hYRmewnJJM="; - }; + src = inputs.tasty-ant-xml; }; text-icu-translit = { - src = pkgs.fetchFromGitHub { - owner = "wireapp"; - repo = "text-icu-translit"; - rev = "317bbd27ea5ae4e7f93836ee9ca664f9bde7c583"; - hash = "sha256-E35PVxi/4iJFfWts3td52KKZKQt4dj9KFP3SvWG77Cc="; - }; - }; - - # open PR https://github.com/yesodweb/wai/pull/958 for sending connection: close when closing connection - warp = { - packages.warp = "warp"; - src = pkgs.fetchFromGitHub { - owner = "yesodweb"; - repo = "wai"; - rev = "ef34334b160c74b62435ccc21f5b458f73506b2f"; - hash = "sha256-7rgZUimPJY+0yVN717pZ2Ep01+XB0z8C/+L9D3Qz9/k="; - }; - }; - - http2 = { - src = fetchgit { - url = "https://github.com/wireapp/http2"; - rev = "ca606d86ed304fa780f7a60d11244019c62a10e0"; - hash = "sha256-eyjFtB28JCcvItZ5R8CT2F5GL62c49oQ49AN8/4HSYw="; - }; + src = inputs.text-icu-translit; }; - # Our fork of 2.0.0. This release hasn't been updated for a while and Nix # is bad in coping with Hackage patched revisions and overriding # ghc-options. So, we have our fork to gain GHC 9.8 compatibility. @@ -245,11 +153,7 @@ let # Can't currently be removed because amazonka-dynamodb-attributevalue # does not exist on hackage amazonka = { - src = fetchgit { - url = "https://github.com/brendanhay/amazonka"; - rev = "a7d699be1076e2aad05a1930ca3937ffea954ad8"; - hash = "sha256-cCRhHH/IgM7tPy8rXHTSRec1zxohO8NWxSVZEG1OjQw="; - }; + src = inputs.amazonka; packages = { amazonka = "lib/amazonka"; amazonka-core = "lib/amazonka-core"; @@ -298,6 +202,15 @@ let version = "0.3.3.1"; sha256 = "sha256-jgSTBBDcxRQ0tjs0wTyvEpEAkGA7npJKjdXDT81VpT4="; }; + + warp = { + version = "3.4.12"; + sha256 = "sha256-Y9xQ1wBbBtSZ4qw3yTGSYX27qi2uFRDJVtAdmQqRnFQ="; + }; + http2 = { + version = "5.4.0"; + sha256 = "sha256-PeEWVd61bQ8G7LvfLeXklzXqNJFaAjE2ecRMWJZESPE="; + }; }; # Name -> Source -> Maybe Subpath -> Drv mkGitDrv = name: src: subpath: diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 764b1b268e2..ea0449d5306 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -8,6 +8,9 @@ hself: hsuper: { # FUTUREWORK: investigate whether all of these tests need to fail # ---------------- + # tests don't work, but only in a flake + saml2-web-sso = hlib.dontCheck hsuper.saml2-web-sso; + # test suite doesn't compile and needs network access bloodhound = hlib.dontCheck hsuper.bloodhound; diff --git a/nix/sources.json b/nix/sources.json deleted file mode 100644 index 60c5cfb6df8..00000000000 --- a/nix/sources.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "nixpkgs": { - "branch": "nixpkgs-unstable", - "description": "Nix Packages collection", - "homepage": "https://github.com/NixOS/nixpkgs", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "09b8fda8959d761445f12b55f380d90375a1d6bb", - "sha256": "0333ri3rmkwlsyvbf8916psydq5i2xq0cj6iis9d6f4ghr19vbva", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/09b8fda8959d761445f12b55f380d90375a1d6bb.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "nixpkgs_24_11": { - "branch": "nixos-24.11", - "description": "Nix Packages collection & NixOS", - "homepage": "", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", - "sha256": "1s2gr5rcyqvpr58vxdcb095mdhblij9bfzaximrva2243aal3dgx", - "type": "tarball", - "url": "https://github.com/nixos/nixpkgs/archive/50ab793786d9de88ee30ec4e4c24fb4236fc2674.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - } -} diff --git a/nix/sources.nix b/nix/sources.nix deleted file mode 100644 index fe3dadf7ebb..00000000000 --- a/nix/sources.nix +++ /dev/null @@ -1,198 +0,0 @@ -# This file has been generated by Niv. - -let - - # - # The fetchers. fetch_ fetches specs of type . - # - - fetch_file = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchurl { inherit (spec) url sha256; name = name'; } - else - pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; - - fetch_tarball = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchTarball { name = name'; inherit (spec) url sha256; } - else - pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; - - fetch_git = name: spec: - let - ref = - spec.ref or ( - if spec ? branch then "refs/heads/${spec.branch}" else - if spec ? tag then "refs/tags/${spec.tag}" else - abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" - ); - submodules = spec.submodules or false; - submoduleArg = - let - nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; - emptyArgWithWarning = - if submodules - then - builtins.trace - ( - "The niv input \"${name}\" uses submodules " - + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " - + "does not support them" - ) - { } - else { }; - in - if nixSupportsSubmodules - then { inherit submodules; } - else emptyArgWithWarning; - in - builtins.fetchGit - ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); - - fetch_local = spec: spec.path; - - fetch_builtin-tarball = name: throw - ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=tarball -a builtin=true''; - - fetch_builtin-url = name: throw - ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=file -a builtin=true''; - - # - # Various helpers - # - - # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 - sanitizeName = name: - ( - concatMapStrings (s: if builtins.isList s then "-" else s) - ( - builtins.split "[^[:alnum:]+._?=-]+" - ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) - ) - ); - - # The set of packages used when specs are fetched using non-builtins. - mkPkgs = sources: system: - let - sourcesNixpkgs = - import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; - hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; - hasThisAsNixpkgsPath = == ./.; - in - if builtins.hasAttr "nixpkgs" sources - then sourcesNixpkgs - else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then - import { } - else - abort - '' - Please specify either (through -I or NIX_PATH=nixpkgs=...) or - add a package called "nixpkgs" to your sources.json. - ''; - - # The actual fetching function. - fetch = pkgs: name: spec: - - if ! builtins.hasAttr "type" spec then - abort "ERROR: niv spec ${name} does not have a 'type' attribute" - else if spec.type == "file" then fetch_file pkgs name spec - else if spec.type == "tarball" then fetch_tarball pkgs name spec - else if spec.type == "git" then fetch_git name spec - else if spec.type == "local" then fetch_local spec - else if spec.type == "builtin-tarball" then fetch_builtin-tarball name - else if spec.type == "builtin-url" then fetch_builtin-url name - else - abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; - - # If the environment variable NIV_OVERRIDE_${name} is set, then use - # the path directly as opposed to the fetched source. - replace = name: drv: - let - saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; - ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; - in - if ersatz == "" then drv else - # this turns the string into an actual Nix path (for both absolute and - # relative paths) - if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; - - # Ports of functions for older nix versions - - # a Nix version of mapAttrs if the built-in doesn't exist - mapAttrs = builtins.mapAttrs or ( - f: set: with builtins; - listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) - ); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 - range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 - stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 - stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); - concatMapStrings = f: list: concatStrings (map f list); - concatStrings = builtins.concatStringsSep ""; - - # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 - optionalAttrs = cond: as: if cond then as else { }; - - # fetchTarball version that is compatible between all the versions of Nix - builtins_fetchTarball = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchTarball; - in - if lessThan nixVersion "1.12" then - fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) - else - fetchTarball attrs; - - # fetchurl version that is compatible between all the versions of Nix - builtins_fetchurl = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchurl; - in - if lessThan nixVersion "1.12" then - fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) - else - fetchurl attrs; - - # Create the final "sources" from the config - mkSources = config: - mapAttrs - ( - name: spec: - if builtins.hasAttr "outPath" spec - then - abort - "The values in sources.json should not have an 'outPath' attribute" - else - spec // { outPath = replace name (fetch config.pkgs name spec); } - ) - config.sources; - - # The "config" used by the fetchers - mkConfig = - { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null - , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile) - , system ? builtins.currentSystem - , pkgs ? mkPkgs sources system - }: rec { - # The sources, i.e. the attribute set of spec name to spec - inherit sources; - - # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers - inherit pkgs; - }; - -in -mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index d360bff24c4..742f63544d1 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -44,6 +44,7 @@ # with nixpkgs' dockerTools to make derivations for docker images that we need. pkgs: pkgs_24_11: +inputs: let inherit (pkgs) lib; hlib = pkgs.haskell.lib; @@ -95,9 +96,7 @@ let inherit (lib) attrsets; pinnedPackages = import ./haskell-pins.nix { - inherit pkgs; - inherit (pkgs) fetchgit; - inherit lib; + inherit lib inputs; }; localPackages = { enableOptimization, enableDocs, enableTests }: hsuper: hself: @@ -325,43 +324,52 @@ let ]; images = localMods@{ enableOptimization, enableDocs, enableTests }: - let exes = staticExecs localMods; + let + exes = staticExecs localMods; + allImages = attrsets.mapAttrs + (execName: drv: + pkgs.dockerTools.streamLayeredImage { + name = "quay.io/wire/${execName}"; + maxLayers = 10; + contents = [ + pkgs.cacert + pkgs.iana-etc + pkgs.dumb-init + pkgs.dockerTools.fakeNss + pkgs.dockerTools.usrBinEnv + drv + tmpDir + ] ++ debugUtils ++ pkgs.lib.optionals (builtins.hasAttr execName (extraContents exes)) (builtins.getAttr execName (extraContents exes)); + # Any mkdir running in this step won't actually make it to the image, + # hence we use the tmpDir derivation in the contents + fakeRootCommands = '' + chmod 1777 tmp + chmod 1777 var/tmp + ''; + config = { + Entrypoint = [ "${pkgs.dumb-init}/bin/dumb-init" "--" "${drv}/bin/${execName}" ]; + Env = [ + "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" + "LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive" + "LANG=en_GB.UTF-8" + # Use stable conventions for tracing http in opentelemetry + # https://opentelemetry.io/blog/2023/http-conventions-declared-stable/#migration-plan + "OTEL_SEMCONV_STABILITY_OPT_IN=http" + ]; + User = "65534"; + }; + } + ) + exes; in - attrsets.mapAttrs - (execName: drv: - pkgs.dockerTools.streamLayeredImage { - name = "quay.io/wire/${execName}"; - maxLayers = 10; - contents = [ - pkgs.cacert - pkgs.iana-etc - pkgs.dumb-init - pkgs.dockerTools.fakeNss - pkgs.dockerTools.usrBinEnv - drv - tmpDir - ] ++ debugUtils ++ pkgs.lib.optionals (builtins.hasAttr execName (extraContents exes)) (builtins.getAttr execName (extraContents exes)); - # Any mkdir running in this step won't actually make it to the image, - # hence we use the tmpDir derivation in the contents - fakeRootCommands = '' - chmod 1777 tmp - chmod 1777 var/tmp - ''; - config = { - Entrypoint = [ "${pkgs.dumb-init}/bin/dumb-init" "--" "${drv}/bin/${execName}" ]; - Env = [ - "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" - "LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive" - "LANG=en_GB.UTF-8" - # Use stable conventions for tracing http in opentelemetry - # https://opentelemetry.io/blog/2023/http-conventions-declared-stable/#migration-plan - "OTEL_SEMCONV_STABILITY_OPT_IN=http" - ]; - User = "65534"; - }; - } - ) - exes; + allImages + // { + all = pkgs.linkFarm "all-images" (attrsets.mapAttrsToList + (name: path: + { inherit name path; } + ) + allImages); + }; localModsEnableAll = { enableOptimization = true; @@ -381,7 +389,7 @@ let imagesList = pkgs.writeTextFile { name = "imagesList"; - text = "${lib.concatStringsSep "\n" (builtins.attrNames (images localModsEnableAll))}"; + text = "${lib.concatStringsSep "\n" (builtins.attrNames (staticExecs localModsEnableAll))}"; }; wireServerPackages = (builtins.attrNames (localPackages localModsEnableAll { } { })); @@ -451,7 +459,7 @@ let bundleNixpkgs = false; extraPkgs = commonTools ++ [ pkgs.cachix ]; nixConf = { - experimental-features = "nix-command"; + experimental-features = "nix-command flakes"; }; }; diff --git a/treefmt.toml b/treefmt.toml index 847bdbb793f..fd38436758e 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -1,9 +1,6 @@ [formatter.nix] command = "nixpkgs-fmt" includes = ["*.nix"] -excludes = [ - "nix/sources.nix" # managed by niv. -] [formatter.cabal-fmt] command = "cabal-fmt" From b3064041ea291faa5bffe95fd747dd28fb3a8108 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Thu, 8 Jan 2026 19:56:48 +0100 Subject: [PATCH 52/60] fix: cabal.project.local check/generation (#4938) --- Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 48f19ae18e0..af09b6ce006 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,6 @@ clean-hint: @echo -e ">>> to never have to remember submodules again, try 'git config --global submodule.recurse true'" @echo -e "\n\n\n" -.PHONY: cabal.project.local cabal.project.local: cp ./hack/cabal.project.local.template ./cabal.project.local @@ -95,11 +94,7 @@ cabal.project.local: c: treefmt c-fast .PHONY: c -c-fast: - if [ ! -e "cabal.project.local" ]; then \ - echo "'cabal.project.local' not found. please run 'make cabal.project.local' and tweak the output to your liking." - exit 1; \ - fi +c-fast: cabal.project.local cabal build $(WIRE_CABAL_BUILD_OPTIONS) $(package) || ( make clean-hint; false ) ifeq ($(test), 1) ./hack/bin/cabal-run-tests.sh $(package) $(testargs) From d7624693baca14c5352c3f5a21a876392775758f Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 9 Jan 2026 11:36:22 +0100 Subject: [PATCH 53/60] [fix] use default values for cells config in swagger example (#4939) --- libs/wire-api/src/Wire/API/Team/Feature.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 5293d5c47b9..99c07fe7b8a 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1720,7 +1720,7 @@ instance Default CellsConfig where instance (FieldF f) => ToSchema (CellsConfigB Covered f) where schema = - object "CellsConfig" $ + objectWithDocModifier "CellsConfig" (S.schema . S.example ?~ schemaToJSON (def @CellsConfig)) $ CellsConfig <$> channels .= fieldF "channels" schema <*> groups .= fieldF "groups" schema From ff8c5017bd370c218e1c49da57f9c1a8662c9476 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 12 Jan 2026 13:39:30 +0100 Subject: [PATCH 54/60] nix: Depend on tom-bombadil using flake input (#4943) --- flake.lock | 21 +++++++++++++++++++++ flake.nix | 10 ++++++++-- nix/default.nix | 4 ++-- nix/wire-server.nix | 8 +++----- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index 19154d022be..68686e15d11 100644 --- a/flake.lock +++ b/flake.lock @@ -240,6 +240,7 @@ "tasty-ant-xml": "tasty-ant-xml", "text-icu-translit": "text-icu-translit", "tinylog": "tinylog", + "tom-bombadil": "tom-bombadil", "wai-predicates": "wai-predicates" } }, @@ -343,6 +344,26 @@ "type": "github" } }, + "tom-bombadil": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1767870783, + "narHash": "sha256-0QStp+uH05bnGltPnOJM2FdeTJdgVIWkVM5wSFYVceM=", + "path": "/home/axeman/workspace/tom-bombadil", + "type": "path" + }, + "original": { + "path": "/home/axeman/workspace/tom-bombadil", + "type": "path" + } + }, "wai-predicates": { "flake": false, "locked": { diff --git a/flake.nix b/flake.nix index 6eb24cb1665..698c5b23d25 100644 --- a/flake.nix +++ b/flake.nix @@ -6,6 +6,11 @@ nixpkgs.url = "github:nixos/nixpkgs?rev=09b8fda8959d761445f12b55f380d90375a1d6bb"; nixpkgs_24_11.url = "github:nixos/nixpkgs?ref=nixos-24.11"; flake-utils.url = "github:numtide/flake-utils"; + tom-bombadil = { + url = "path:/home/axeman/workspace/tom-bombadil"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.flake-utils.follows = "flake-utils"; + }; cryptobox-haskell = { url = "github:wireapp/cryptobox-haskell?ref=master"; @@ -85,7 +90,7 @@ }; }; - outputs = inputs@{ nixpkgs, nixpkgs_24_11, flake-utils, ... }: + outputs = inputs@{ nixpkgs, nixpkgs_24_11, flake-utils, tom-bombadil, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { @@ -98,7 +103,8 @@ pkgs_24_11 = import nixpkgs_24_11 { inherit system; }; - wireServerPkgs = import ./nix { inherit pkgs pkgs_24_11 inputs; }; + bomDependenciesDrv = tom-bombadil.lib.${system}.bomDependenciesDrv; + wireServerPkgs = import ./nix { inherit pkgs pkgs_24_11 inputs bomDependenciesDrv; }; in { # profileEnv wireServer docs docsEnv mls-test-cli nginz; diff --git a/nix/default.nix b/nix/default.nix index 159199845c5..71f9845b8d9 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,4 +1,4 @@ -{ pkgs, pkgs_24_11, inputs }: +{ pkgs, pkgs_24_11, bomDependenciesDrv, inputs, }: let profileEnv = pkgs.writeTextFile { name = "profile-env"; @@ -10,7 +10,7 @@ let ''; }; - wireServer = import ./wire-server.nix pkgs pkgs_24_11 inputs; + wireServer = import ./wire-server.nix { inherit pkgs pkgs_24_11 bomDependenciesDrv inputs; }; nginz = pkgs.callPackage ./nginz.nix { }; # packages necessary to build wire-server docs diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 742f63544d1..dd2b183877f 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -42,9 +42,8 @@ # Using these tweaks we can get a haskell package set which has wire-server # components and the required dependencies. We then use this package set along # with nixpkgs' dockerTools to make derivations for docker images that we need. -pkgs: -pkgs_24_11: -inputs: + +{ pkgs, pkgs_24_11, bomDependenciesDrv, inputs, }: let inherit (pkgs) lib; hlib = pkgs.haskell.lib; @@ -484,9 +483,8 @@ let haskellPackages = hPkgs localModsEnableAll; haskellPackagesUnoptimizedNoDocs = hPkgs localModsOnlyTests; - tom-bombadil = builtins.getFlake "github:wireapp/tom-bombadil"; localPkgs = map (e: (hPkgs localModsEnableAll).${e}) wireServerPackages; - bomDependencies = tom-bombadil.lib.${builtins.currentSystem}.bomDependenciesDrv pkgs localPkgs haskellPackages; + bomDependencies = bomDependenciesDrv pkgs localPkgs haskellPackages; in { inherit ciImage hoogleImage allImages haskellPackages haskellPackagesUnoptimizedNoDocs imagesList bomDependencies; From da544f2c57e7f25ad5b6e0c27055b525cc4b7df1 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 12 Jan 2026 14:13:54 +0100 Subject: [PATCH 55/60] Finalize API version v14 and create new dev version v15 (#4942) --- changelog.d/1-api-changes/ WPB-22702 | 1 + integration/test/Test/Swagger.hs | 2 +- integration/test/Testlib/Env.hs | 2 +- libs/wire-api/src/Wire/API/Routes/Version.hs | 8 +- services/brig/docs/swagger-v14.json | 38256 +++++++++++++++++ services/brig/src/Brig/API/Public.hs | 23 +- 6 files changed, 38277 insertions(+), 15 deletions(-) create mode 100644 changelog.d/1-api-changes/ WPB-22702 create mode 100644 services/brig/docs/swagger-v14.json diff --git a/changelog.d/1-api-changes/ WPB-22702 b/changelog.d/1-api-changes/ WPB-22702 new file mode 100644 index 00000000000..d8806e9870d --- /dev/null +++ b/changelog.d/1-api-changes/ WPB-22702 @@ -0,0 +1 @@ +Create new API version V15 and finalize API version V14 diff --git a/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs index 6b64f9bb924..0b7ce31ec02 100644 --- a/integration/test/Test/Swagger.hs +++ b/integration/test/Test/Swagger.hs @@ -30,7 +30,7 @@ import Testlib.Prelude import UnliftIO.Temporary existingVersions :: Set Int -existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] +existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] internalApis :: Set String internalApis = Set.fromList ["brig", "cannon", "cargohold", "cannon", "spar"] diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 67a90228ad0..1a65d7f5ea1 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -133,7 +133,7 @@ mkGlobalEnv cfgFile = do gFederationV1Domain = intConfig.federationV1.originDomain, gFederationV2Domain = intConfig.federationV2.originDomain, gDynamicDomains = (.domain) <$> Map.elems intConfig.dynamicBackends, - gDefaultAPIVersion = 14, + gDefaultAPIVersion = 15, gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), gBackendResourcePool = resourcePool, diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index b251e039bf6..d54784f796b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -98,7 +98,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) -- and 'developmentVersions' stay in sync; everything else here should keep working without -- change. See also documentation in the *docs* directory. -- https://docs.wire.com/developer/developer/api-versioning.html#version-bump-checklist -data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 +data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 | V15 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) deriving (Arbitrary) via (GenericUniform Version) @@ -131,6 +131,8 @@ instance RenderableSymbol V13 where renderSymbol = "V13" instance RenderableSymbol V14 where renderSymbol = "V14" +instance RenderableSymbol V15 where renderSymbol = "V15" + -- | Manual enumeration of version integrals (the `` in the constructor `V`). -- -- This is not the same as 'fromEnum': we will remove unsupported versions in the future, @@ -153,6 +155,7 @@ versionInt V11 = 11 versionInt V12 = 12 versionInt V13 = 13 versionInt V14 = 14 +versionInt V15 = 15 supportedVersions :: [Version] supportedVersions = [minBound .. maxBound] @@ -274,7 +277,8 @@ isDevelopmentVersion V10 = False isDevelopmentVersion V11 = False isDevelopmentVersion V12 = False isDevelopmentVersion V13 = False -isDevelopmentVersion V14 = True +isDevelopmentVersion V14 = False +isDevelopmentVersion V15 = True developmentVersions :: [Version] developmentVersions = filter isDevelopmentVersion supportedVersions diff --git a/services/brig/docs/swagger-v14.json b/services/brig/docs/swagger-v14.json new file mode 100644 index 00000000000..2b31a47a4be --- /dev/null +++ b/services/brig/docs/swagger-v14.json @@ -0,0 +1,38256 @@ +{ + "components": { + "schemas": { + "ASCII": { + "example": "aGVsbG8", + "type": "string" + }, + "AcceptTeamInvitation": { + "description": "Accept an invitation to join a team on Wire.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "description": "The user account password.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "Access": { + "description": "How users can join conversations", + "enum": [ + "private", + "invite", + "link", + "code" + ], + "type": "string" + }, + "AccessRole": { + "description": "Which users/services can join conversations. This replaces legacy access roles and allows a more fine grained configuration of access roles, and in particular a separation of guest and services access.\n\nThis field is optional. If it is not present, the default will be `[team_member, non_team_member, service]`. Please note that an empty list is not allowed when creating a new conversation.", + "enum": [ + "team_member", + "non_team_member", + "guest", + "service" + ], + "type": "string" + }, + "AccessRoleLegacy": { + "deprecated": true, + "description": "Deprecated, please use access_role_v2", + "enum": [ + "private", + "team", + "activated", + "non_activated" + ], + "type": "string" + }, + "AccessToken": { + "properties": { + "access_token": { + "description": "The opaque access token string", + "type": "string" + }, + "expires_in": { + "description": "The number of seconds this token is valid", + "type": "integer" + }, + "token_type": { + "$ref": "#/components/schemas/TokenType" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "access_token", + "token_type", + "expires_in" + ], + "type": "object" + }, + "AccessTokenType": { + "enum": [ + "DPoP" + ], + "type": "string" + }, + "AccountStatus": { + "enum": [ + "active", + "suspended", + "deleted", + "ephemeral", + "pending-invitation" + ], + "type": "string" + }, + "Action": { + "enum": [ + "add_conversation_member", + "remove_conversation_member", + "modify_conversation_name", + "modify_conversation_message_timer", + "modify_conversation_receipt_mode", + "modify_conversation_access", + "modify_other_conversation_member", + "leave_conversation", + "delete_conversation", + "modify_add_permission" + ], + "type": "string" + }, + "Activate": { + "description": "Data for an activation request.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "dryrun": { + "description": "At least one of key, email, or phone has to be present while key takes precedence over email, and email takes precedence over phone. Whether to perform a dryrun, i.e. to only check whether activation would succeed. Dry-runs never issue access cookies or tokens on success but failures still count towards the maximum failure count.", + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "code", + "dryrun" + ], + "type": "object" + }, + "ActivationResponse": { + "description": "Response body of a successful activation request", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "first": { + "description": "Whether this is the first successful activation (i.e. account activation).", + "type": "boolean" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + } + }, + "type": "object" + }, + "AddBot": { + "properties": { + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "service": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "provider", + "service" + ], + "type": "object" + }, + "AddBotResponse": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "event": { + "$ref": "#/components/schemas/Event" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "client", + "name", + "accent_id", + "assets", + "event" + ], + "type": "object" + }, + "AddPermission": { + "enum": [ + "admins", + "everyone" + ], + "type": "string" + }, + "AddPermissionUpdate": { + "description": "The action of changing the permission to add members to a channel", + "properties": { + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + } + }, + "required": [ + "add_permission" + ], + "type": "object" + }, + "AllTeamFeatures": { + "properties": { + "allowedGlobalOperations": { + "$ref": "#/components/schemas/AllowedGlobalOperationsConfig.LockableFeature" + }, + "appLock": { + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" + }, + "apps": { + "$ref": "#/components/schemas/AppsConfig.LockableFeature" + }, + "assetAuditLog": { + "$ref": "#/components/schemas/AssetAuditLogConfig.LockableFeature" + }, + "cells": { + "$ref": "#/components/schemas/CellsConfig.LockableFeature" + }, + "cellsInternal": { + "$ref": "#/components/schemas/CellsInternalConfig.LockableFeature" + }, + "channels": { + "$ref": "#/components/schemas/ChannelsConfig.LockableFeature" + }, + "chatBubbles": { + "$ref": "#/components/schemas/ChatBubblesConfig.LockableFeature" + }, + "classifiedDomains": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig.LockableFeature" + }, + "conferenceCalling": { + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + }, + "consumableNotifications": { + "$ref": "#/components/schemas/ConsumableNotificationsConfig.LockableFeature" + }, + "conversationGuestLinks": { + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + }, + "digitalSignatures": { + "$ref": "#/components/schemas/DigitalSignaturesConfig.LockableFeature" + }, + "domainRegistration": { + "$ref": "#/components/schemas/DomainRegistrationConfig.LockableFeature" + }, + "enforceFileDownloadLocation": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" + }, + "exposeInvitationURLsToTeamAdmin": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" + }, + "fileSharing": { + "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" + }, + "legalhold": { + "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" + }, + "limitedEventFanout": { + "$ref": "#/components/schemas/LimitedEventFanoutConfig.LockableFeature" + }, + "meetings": { + "$ref": "#/components/schemas/MeetingsConfig.LockableFeature" + }, + "meetingsPremium": { + "$ref": "#/components/schemas/MeetingsPremiumConfig.LockableFeature" + }, + "mls": { + "$ref": "#/components/schemas/MLSConfig.LockableFeature" + }, + "mlsE2EId": { + "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" + }, + "mlsMigration": { + "$ref": "#/components/schemas/MlsMigration.LockableFeature" + }, + "outlookCalIntegration": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" + }, + "searchVisibility": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" + }, + "searchVisibilityInbound": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" + }, + "selfDeletingMessages": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" + }, + "simplifiedUserConnectionRequestQRCode": { + "$ref": "#/components/schemas/SimplifiedUserConnectionRequestQRCode.LockableFeature" + }, + "sndFactorPasswordChallenge": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" + }, + "sso": { + "$ref": "#/components/schemas/SSOConfig.LockableFeature" + }, + "stealthUsers": { + "$ref": "#/components/schemas/StealthUsersConfig.LockableFeature" + }, + "validateSAMLemails": { + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" + } + }, + "required": [ + "legalhold", + "sso", + "searchVisibility", + "searchVisibilityInbound", + "validateSAMLemails", + "digitalSignatures", + "appLock", + "fileSharing", + "classifiedDomains", + "conferenceCalling", + "selfDeletingMessages", + "conversationGuestLinks", + "sndFactorPasswordChallenge", + "mls", + "exposeInvitationURLsToTeamAdmin", + "outlookCalIntegration", + "mlsE2EId", + "mlsMigration", + "enforceFileDownloadLocation", + "limitedEventFanout", + "domainRegistration", + "channels", + "cells", + "allowedGlobalOperations", + "consumableNotifications", + "chatBubbles", + "apps", + "simplifiedUserConnectionRequestQRCode", + "assetAuditLog", + "stealthUsers", + "cellsInternal", + "meetings", + "meetingsPremium" + ], + "type": "object" + }, + "AllowedGlobalOperationsConfig": { + "properties": { + "mlsConversationReset": { + "type": "boolean" + } + }, + "required": [ + "mlsConversationReset" + ], + "type": "object" + }, + "AllowedGlobalOperationsConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/AllowedGlobalOperationsConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "AppLockConfig": { + "properties": { + "enforceAppLock": { + "type": "boolean" + }, + "inactivityTimeoutSecs": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforceAppLock", + "inactivityTimeoutSecs" + ], + "type": "object" + }, + "AppLockConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "AppLockConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "ApproveLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "AppsConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Asset": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "expires": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "key": { + "$ref": "#/components/schemas/AssetKey" + }, + "token": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "domain" + ], + "type": "object" + }, + "AssetAuditLogConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "AssetKey": { + "description": "S3 asset key for an icon image with retention information.", + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "AssetSize": { + "enum": [ + "preview", + "complete" + ], + "type": "string" + }, + "AssetSource": {}, + "AssetType": { + "enum": [ + "image" + ], + "type": "string" + }, + "AuthnRequest": { + "properties": { + "iD": { + "$ref": "#/components/schemas/Id_AuthnRequest" + }, + "issueInstant": { + "$ref": "#/components/schemas/Time" + }, + "issuer": { + "type": "string" + }, + "nameIDPolicy": { + "$ref": "#/components/schemas/NameIdPolicy" + } + }, + "required": [ + "iD", + "issueInstant", + "issuer" + ], + "type": "object" + }, + "BackendConfig": { + "properties": { + "config_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_url" + ], + "type": "object" + }, + "Base64ByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "Base64URLByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "BaseProtocol": { + "enum": [ + "proteus", + "mls" + ], + "type": "string" + }, + "BindingNewTeamUser": { + "properties": { + "currency": { + "$ref": "#/components/schemas/Currency.Alpha" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "description": "The decryption key for the team icon S3 asset", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "description": "team name", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "icon" + ], + "type": "object" + }, + "BotConvView": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "members": { + "items": { + "$ref": "#/components/schemas/OtherMember" + }, + "type": "array" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "members" + ], + "type": "object" + }, + "BotUserView": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "name", + "accent_id" + ], + "type": "object" + }, + "Category": { + "enum": [ + "security", + "collaboration", + "productivity", + "automation", + "files", + "ai", + "developer", + "support", + "finance", + "hr", + "integration", + "compliance", + "other" + ], + "type": "string" + }, + "CellsBackend": { + "properties": { + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "url" + ], + "type": "object" + }, + "CellsCollabora": { + "properties": { + "edition": { + "$ref": "#/components/schemas/CollaboraEdition" + } + }, + "required": [ + "edition" + ], + "type": "object" + }, + "CellsCollaboraStatus": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "CellsConfig": { + "example": { + "channels": { + "default": "enabled", + "enabled": true + }, + "collabora": { + "enabled": false + }, + "groups": { + "default": "enabled", + "enabled": true + }, + "metadata": { + "namespaces": { + "usermetaTags": { + "allowFreeValues": true, + "defaultValues": [] + } + } + }, + "one2one": { + "default": "enabled", + "enabled": true + }, + "publicLinks": { + "enableFiles": true, + "enableFolders": true, + "enforceExpirationDefault": 0, + "enforceExpirationMax": 0, + "enforcePassword": false + }, + "storage": { + "perFileQuotaBytes": "100000000", + "recycle": { + "allowSkip": false, + "autoPurgeDays": 30, + "disable": false + } + }, + "users": { + "externals": true, + "guests": false + } + }, + "properties": { + "channels": { + "$ref": "#/components/schemas/CellsProperty" + }, + "collabora": { + "$ref": "#/components/schemas/CellsCollaboraStatus" + }, + "groups": { + "$ref": "#/components/schemas/CellsProperty" + }, + "metadata": { + "$ref": "#/components/schemas/CellsMetadata" + }, + "one2one": { + "$ref": "#/components/schemas/CellsProperty" + }, + "publicLinks": { + "$ref": "#/components/schemas/CellsPublicLinks" + }, + "storage": { + "$ref": "#/components/schemas/CellsConfigStorage" + }, + "users": { + "$ref": "#/components/schemas/CellsUsers" + } + }, + "required": [ + "channels", + "groups", + "one2one", + "users", + "collabora", + "publicLinks", + "storage", + "metadata" + ], + "type": "object" + }, + "CellsConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "CellsConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "CellsConfigStorage": { + "properties": { + "perFileQuotaBytes": { + "type": "string" + }, + "recycle": { + "$ref": "#/components/schemas/CellsRecycle" + } + }, + "required": [ + "perFileQuotaBytes", + "recycle" + ], + "type": "object" + }, + "CellsInternalConfig": { + "properties": { + "backend": { + "$ref": "#/components/schemas/CellsBackend" + }, + "collabora": { + "$ref": "#/components/schemas/CellsCollabora" + }, + "storage": { + "$ref": "#/components/schemas/CellsStorage" + } + }, + "required": [ + "backend", + "collabora", + "storage" + ], + "type": "object" + }, + "CellsInternalConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsInternalConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "CellsMetadata": { + "properties": { + "namespaces": { + "$ref": "#/components/schemas/CellsNamespaces" + } + }, + "required": [ + "namespaces" + ], + "type": "object" + }, + "CellsNamespaces": { + "properties": { + "usermetaTags": { + "$ref": "#/components/schemas/CellsUserMetaTags" + } + }, + "required": [ + "usermetaTags" + ], + "type": "object" + }, + "CellsProperty": { + "properties": { + "default": { + "$ref": "#/components/schemas/CellsPropertyStatus" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled", + "default" + ], + "type": "object" + }, + "CellsPropertyStatus": { + "enum": [ + "enabled", + "disabled", + "enforced" + ], + "type": "string" + }, + "CellsPublicLinks": { + "properties": { + "enableFiles": { + "type": "boolean" + }, + "enableFolders": { + "type": "boolean" + }, + "enforceExpirationDefault": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "enforceExpirationMax": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "enforcePassword": { + "type": "boolean" + } + }, + "required": [ + "enableFiles", + "enableFolders", + "enforcePassword", + "enforceExpirationMax", + "enforceExpirationDefault" + ], + "type": "object" + }, + "CellsRecycle": { + "properties": { + "allowSkip": { + "type": "boolean" + }, + "autoPurgeDays": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "disable": { + "type": "boolean" + } + }, + "required": [ + "autoPurgeDays", + "disable", + "allowSkip" + ], + "type": "object" + }, + "CellsState": { + "enum": [ + "disabled", + "pending", + "ready" + ], + "type": "string" + }, + "CellsStorage": { + "properties": { + "teamQuotaBytes": { + "type": "string" + } + }, + "required": [ + "teamQuotaBytes" + ], + "type": "object" + }, + "CellsUserMetaTags": { + "properties": { + "allowFreeValues": { + "type": "boolean" + }, + "defaultValues": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "defaultValues", + "allowFreeValues" + ], + "type": "object" + }, + "CellsUsers": { + "properties": { + "externals": { + "type": "boolean" + }, + "guests": { + "type": "boolean" + } + }, + "required": [ + "externals", + "guests" + ], + "type": "object" + }, + "ChallengeToken": { + "properties": { + "challenge_token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "challenge_token" + ], + "type": "object" + }, + "ChannelPermissions": { + "enum": [ + "team-members", + "everyone", + "admins" + ], + "type": "string" + }, + "ChannelsConfig": { + "properties": { + "allowed_to_create_channels": { + "$ref": "#/components/schemas/ChannelPermissions" + }, + "allowed_to_open_channels": { + "$ref": "#/components/schemas/ChannelPermissions" + } + }, + "required": [ + "allowed_to_create_channels", + "allowed_to_open_channels" + ], + "type": "object" + }, + "ChannelsConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ChannelsConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "ChannelsConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ChannelsConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "ChatBubblesConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "CheckHandles": { + "properties": { + "handles": { + "items": { + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array" + }, + "return": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "handles", + "return" + ], + "type": "object" + }, + "CheckUserGroupName": { + "properties": { + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "CipherSuiteTag": { + "description": "The cipher suite of the corresponding MLS group", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ClassifiedDomainsConfig": { + "properties": { + "domains": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "domains" + ], + "type": "object" + }, + "ClassifiedDomainsConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "Client": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "cookie": { + "type": "string" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "label": { + "type": "string" + }, + "last_active": { + "$ref": "#/components/schemas/UTCTime" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/ClientType" + } + }, + "required": [ + "id", + "type", + "time" + ], + "type": "object" + }, + "ClientCapability": { + "enum": [ + "legalhold-implicit-consent", + "consumable-notifications" + ], + "type": "string" + }, + "ClientCapabilityList": { + "items": { + "$ref": "#/components/schemas/ClientCapability" + }, + "type": "array" + }, + "ClientClass": { + "enum": [ + "phone", + "tablet", + "desktop", + "legalhold" + ], + "type": "string" + }, + "ClientIdentity": { + "properties": { + "client_id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "user_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "user_id", + "client_id" + ], + "type": "object" + }, + "ClientMismatch": { + "properties": { + "deleted": { + "$ref": "#/components/schemas/UserClients" + }, + "missing": { + "$ref": "#/components/schemas/UserClients" + }, + "redundant": { + "$ref": "#/components/schemas/UserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted" + ], + "type": "object" + }, + "ClientPrekey": { + "properties": { + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "prekey": { + "$ref": "#/components/schemas/Prekey" + } + }, + "required": [ + "client", + "prekey" + ], + "type": "object" + }, + "ClientType": { + "enum": [ + "temporary", + "permanent", + "legalhold" + ], + "type": "string" + }, + "CodeChallengeMethod": { + "description": "The method used to encode the code challenge. Only `S256` is supported.", + "enum": [ + "S256" + ], + "type": "string" + }, + "CollaboraEdition": { + "enum": [ + "NO", + "CODE", + "COOL" + ], + "type": "string" + }, + "CollaboratorPermission": { + "enum": [ + "create_team_conversation", + "implicit_connection" + ], + "type": "string" + }, + "CommitBundle": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "CompletePasswordReset": { + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "key", + "code", + "password" + ], + "type": "object" + }, + "ConferenceCallingConfig": { + "properties": { + "useSFTForOneToOneCalls": { + "type": "boolean" + } + }, + "type": "object" + }, + "ConferenceCallingConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ConferenceCallingConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Connect": { + "properties": { + "email": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "qualified_recipient": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "recipient": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_recipient" + ], + "type": "object" + }, + "ConnectionUpdate": { + "properties": { + "status": { + "$ref": "#/components/schemas/Relation" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Connections_Page": { + "properties": { + "connections": { + "items": { + "$ref": "#/components/schemas/UserConnection" + }, + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/Connections_PagingState" + } + }, + "required": [ + "connections", + "has_more", + "paging_state" + ], + "type": "object" + }, + "Connections_PagingState": { + "type": "string" + }, + "ConsumableNotificationsConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Contact": { + "description": "Contact discovered through search", + "properties": { + "accent_id": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_id", + "name" + ], + "type": "object" + }, + "ConvMembers": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/components/schemas/OtherMember" + }, + "type": "array" + }, + "self": { + "$ref": "#/components/schemas/Member" + } + }, + "required": [ + "others" + ], + "type": "object" + }, + "ConvTeamInfo": { + "description": "Team information of this conversation", + "properties": { + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + }, + "teamid": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "teamid", + "managed" + ], + "type": "object" + }, + "ConvType": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer" + }, + "Conversation": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "ConversationAccessData": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access", + "access_role" + ], + "type": "object" + }, + "ConversationAccessDataV2": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access" + ], + "type": "object" + }, + "ConversationCode": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "ConversationCodeInfo": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "key", + "code", + "uri", + "has_password" + ], + "type": "object" + }, + "ConversationCoverView": { + "description": "Limited view of Conversation.", + "properties": { + "has_password": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "has_password" + ], + "type": "object" + }, + "ConversationIds_Page": { + "properties": { + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/ConversationIds_PagingState" + }, + "qualified_conversations": { + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "qualified_conversations", + "has_more", + "paging_state" + ], + "type": "object" + }, + "ConversationIds_PagingState": { + "type": "string" + }, + "ConversationMessageTimerUpdate": { + "description": "Contains conversation properties to update", + "properties": { + "message_timer": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "type": "object" + }, + "ConversationPage": { + "description": "This is the last page if it contains fewer rows than requested. There may be 0 rows on a page.", + "properties": { + "page": { + "items": { + "$ref": "#/components/schemas/ConversationSearchResult" + }, + "type": "array" + } + }, + "required": [ + "page" + ], + "type": "object" + }, + "ConversationReceiptModeUpdate": { + "description": "Contains conversation receipt mode to update to. Receipt mode tells clients whether certain types of receipts should be sent in the given conversation or not. How this value is interpreted is up to clients.", + "properties": { + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "receipt_mode" + ], + "type": "object" + }, + "ConversationRename": { + "properties": { + "name": { + "description": "The new conversation name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ConversationReset": { + "properties": { + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "new_group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id" + ], + "type": "object" + }, + "ConversationRole": { + "properties": { + "actions": { + "description": "The set of actions allowed for this role", + "items": { + "$ref": "#/components/schemas/Action" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + } + } + }, + "ConversationRolesList": { + "properties": { + "conversation_roles": { + "items": { + "$ref": "#/components/schemas/ConversationRole" + }, + "type": "array" + } + }, + "required": [ + "conversation_roles" + ], + "type": "object" + }, + "ConversationSearchResult": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "admin_count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "member_count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "access", + "member_count", + "admin_count" + ], + "type": "object" + }, + "ConversationsResponse": { + "description": "Response object for getting metadata of a list of conversations", + "properties": { + "failed": { + "description": "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server", + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/components/schemas/OwnConversation" + }, + "type": "array" + }, + "not_found": { + "description": "These conversations either don't exist or are deleted.", + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "found", + "not_found", + "failed" + ], + "type": "object" + }, + "Cookie": { + "properties": { + "created": { + "$ref": "#/components/schemas/UTCTime" + }, + "expires": { + "$ref": "#/components/schemas/UTCTime" + }, + "id": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "label": { + "type": "string" + }, + "successor": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": { + "$ref": "#/components/schemas/CookieType" + } + }, + "required": [ + "id", + "type", + "created", + "expires" + ], + "type": "object" + }, + "CookieList": { + "description": "List of cookie information", + "properties": { + "cookies": { + "items": { + "$ref": "#/components/schemas/Cookie" + }, + "type": "array" + } + }, + "required": [ + "cookies" + ], + "type": "object" + }, + "CookieType": { + "enum": [ + "session", + "persistent" + ], + "type": "string" + }, + "CreateConversationCodeRequest": { + "description": "Request body for creating a conversation code", + "properties": { + "password": { + "description": "Password for accessing the conversation via guest link. Set to null or omit for no password.", + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "type": "object" + }, + "CreateGroupConversation": { + "description": "A created group-conversation object extended with a list of failed-to-add users", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "failed_to_add": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "failed_to_add" + ], + "type": "object" + }, + "CreateOAuthAuthorizationCodeRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "code_challenge": { + "$ref": "#/components/schemas/OAuthCodeChallenge" + }, + "code_challenge_method": { + "$ref": "#/components/schemas/CodeChallengeMethod" + }, + "redirect_uri": { + "$ref": "#/components/schemas/RedirectUrl" + }, + "response_type": { + "$ref": "#/components/schemas/OAuthResponseType" + }, + "scope": { + "description": "The scopes which are requested to get authorization for, separated by a space", + "type": "string" + }, + "state": { + "description": "An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery", + "type": "string" + } + }, + "required": [ + "client_id", + "scope", + "response_type", + "redirect_uri", + "state", + "code_challenge_method", + "code_challenge" + ], + "type": "object" + }, + "CreateScimToken": { + "properties": { + "description": { + "type": "string" + }, + "idp": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "description" + ], + "type": "object" + }, + "CreateScimTokenResponse": { + "properties": { + "info": { + "$ref": "#/components/schemas/ScimTokenInfo" + }, + "token": { + "type": "string" + } + }, + "required": [ + "token", + "info" + ], + "type": "object" + }, + "CreateUserTeam": { + "properties": { + "team_id": { + "$ref": "#/components/schemas/UUID" + }, + "team_name": { + "type": "string" + } + }, + "required": [ + "team_id", + "team_name" + ], + "type": "object" + }, + "CreatedApp": { + "properties": { + "cookie": { + "$ref": "#/components/schemas/SomeUserToken" + }, + "user": { + "$ref": "#/components/schemas/User" + } + }, + "required": [ + "user", + "cookie" + ], + "type": "object" + }, + "Currency.Alpha": { + "description": "ISO 4217 alphabetic codes. This is only stored by the backend, not processed. It can be removed once billing supports currency changes after team creation.", + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "XSU", + "XTS", + "XUA", + "XXX", + "YER", + "ZAR", + "ZMW", + "ZWL" + ], + "example": "EUR", + "type": "string" + }, + "CustomBackend": { + "description": "Description of a custom backend", + "properties": { + "config_json_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_welcome_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_json_url", + "webapp_welcome_url" + ], + "type": "object" + }, + "DPoPAccessToken": { + "type": "string" + }, + "DPoPAccessTokenResponse": { + "properties": { + "expires_in": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "token": { + "$ref": "#/components/schemas/DPoPAccessToken" + }, + "type": { + "$ref": "#/components/schemas/AccessTokenType" + } + }, + "required": [ + "token", + "type", + "expires_in" + ], + "type": "object" + }, + "DeleteClient": { + "properties": { + "password": { + "description": "The password of the authenticated user for verification. The password is not required for deleting temporary clients.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeleteKeyPackages": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackageRef" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "DeleteProvider": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "DeleteService": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "DeleteUser": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeletionCodeTimeout": { + "properties": { + "expires_in": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "DigitalSignaturesConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "DisableLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Domain": { + "example": "example.com", + "type": "string" + }, + "DomainOwnershipToken": { + "properties": { + "domain_ownership_token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "domain_ownership_token" + ], + "type": "object" + }, + "DomainRedirect Tag": { + "enum": [ + "none", + "locked", + "sso", + "backend", + "no-registration", + "pre-authorized" + ], + "type": "string" + }, + "DomainRedirectConfig": { + "properties": { + "backend": { + "$ref": "#/components/schemas/backend_config" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirectConfigTag" + } + }, + "required": [ + "domain_redirect", + "backend" + ], + "type": "object" + }, + "DomainRedirectConfigTag": { + "enum": [ + "remove", + "backend", + "no-registration" + ], + "type": "string" + }, + "DomainRedirectResponseV10": { + "properties": { + "backend": { + "$ref": "#/components/schemas/BackendConfig" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirect Tag" + }, + "due_to_existing_account": { + "type": "boolean" + }, + "sso_code": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain_redirect", + "sso_code", + "backend" + ], + "type": "object" + }, + "DomainRegistrationConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "DomainRegistrationResponse": { + "properties": { + "authorized_team": { + "$ref": "#/components/schemas/UUID" + }, + "backend": { + "$ref": "#/components/schemas/BackendConfig" + }, + "dns_verification_token": { + "$ref": "#/components/schemas/ASCII" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirect Tag" + }, + "sso_code": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "team_invite": { + "$ref": "#/components/schemas/TeamInvite Tag" + } + }, + "required": [ + "domain", + "domain_redirect", + "sso_code", + "backend", + "team_invite", + "team" + ], + "type": "object" + }, + "DomainVerificationChallenge": { + "properties": { + "dns_verification_token": { + "$ref": "#/components/schemas/ASCII" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "id", + "token", + "dns_verification_token" + ], + "type": "object" + }, + "EdMemberLeftReason": { + "enum": [ + "left", + "user-deleted", + "removed" + ], + "type": "string" + }, + "Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest": { + "oneOf": [ + { + "properties": { + "Left": { + "$ref": "#/components/schemas/OAuthAccessTokenRequest" + } + }, + "required": [ + "Left" + ], + "title": "Left", + "type": "object" + }, + { + "properties": { + "Right": { + "$ref": "#/components/schemas/OAuthRefreshAccessTokenRequest" + } + }, + "required": [ + "Right" + ], + "title": "Right", + "type": "object" + } + ] + }, + "Email": { + "type": "string" + }, + "EmailUpdate": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "EnforceFileDownloadLocation": { + "properties": { + "enforcedDownloadLocation": { + "type": "string" + } + }, + "type": "object" + }, + "EnforceFileDownloadLocation.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "EnforceFileDownloadLocation.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "EpochTimestamp": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "Event": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "data": { + "description": "The action of changing the permission to add members to a channel", + "example": "ZXhhbXBsZQo=", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "add_type": { + "$ref": "#/components/schemas/JoinType" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "email": { + "type": "string" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message": { + "type": "string" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "new_group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_recipient": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "qualified_target": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "recipient": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/TypingStatus" + }, + "target": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users", + "add_type", + "reason", + "qualified_user_ids", + "user_ids", + "qualified_target", + "name", + "access", + "key", + "code", + "uri", + "has_password", + "qualified_id", + "type", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite", + "qualified_recipient", + "receipt_mode", + "sender", + "recipient", + "text", + "status", + "add_permission" + ], + "type": "object" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_from": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "subconv": { + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/EventType" + }, + "via": { + "$ref": "#/components/schemas/EventVia" + } + }, + "required": [ + "type", + "data", + "qualified_conversation", + "qualified_from", + "via", + "time" + ], + "type": "object" + }, + "EventType": { + "enum": [ + "conversation.member-join", + "conversation.member-leave", + "conversation.member-update", + "conversation.rename", + "conversation.access-update", + "conversation.receipt-mode-update", + "conversation.message-timer-update", + "conversation.code-update", + "conversation.code-delete", + "conversation.create", + "conversation.delete", + "conversation.mls-reset", + "conversation.connect-request", + "conversation.typing", + "conversation.otr-message-add", + "conversation.mls-message-add", + "conversation.mls-welcome", + "conversation.protocol-update", + "conversation.add-permission-update" + ], + "type": "string" + }, + "EventVia": { + "enum": [ + "scim", + "user" + ], + "type": "string" + }, + "ExposeInvitationURLsToTeamAdminConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ExposeInvitationURLsToTeamAdminConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "FeatureStatus": { + "enum": [ + "enabled", + "disabled" + ], + "type": "string" + }, + "FederatedUserSearchPolicy": { + "description": "Search policy that was applied when searching for users", + "enum": [ + "no_search", + "exact_handle_search", + "full_search" + ], + "type": "string" + }, + "FileSharingConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "FileSharingConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Fingerprint": { + "example": "ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=", + "type": "string" + }, + "FormRedirect": { + "properties": { + "uri": { + "type": "string" + }, + "xml": { + "$ref": "#/components/schemas/AuthnRequest" + } + }, + "type": "object" + }, + "GetApp": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "description": { + "maxLength": 300, + "minLength": 0, + "type": "string" + }, + "metadata": { + "type": "object" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + } + }, + "required": [ + "name", + "metadata", + "category", + "description" + ], + "type": "object" + }, + "GetDomainRegistrationRequest": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "GetPaginated_Connections": { + "description": "A request to list some or all of a user's Connections, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/components/schemas/Connections_PagingState" + }, + "size": { + "description": "optional, must be <= 500, defaults to 100.", + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GetPaginated_ConversationIds": { + "description": "A request to list some or all of a user's ConversationIds, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/components/schemas/ConversationIds_PagingState" + }, + "size": { + "description": "optional, must be <= 1000, defaults to 1000.", + "format": "int32", + "maximum": 1000, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GroupConvType": { + "enum": [ + "group_conversation", + "channel" + ], + "type": "string" + }, + "GroupId": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "GroupInfoData": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "GuestLinksConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "GuestLinksConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Handle": { + "type": "string" + }, + "HandleUpdate": { + "properties": { + "handle": { + "type": "string" + } + }, + "required": [ + "handle" + ], + "type": "object" + }, + "HttpsUrl": { + "example": "https://example.com", + "type": "string" + }, + "Icon": { + "description": "S3 asset key for an icon image with retention information. Allows special value 'default'.", + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "Id": { + "properties": { + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "IdPConfig_WireIdP": { + "properties": { + "extraInfo": { + "$ref": "#/components/schemas/WireIdP" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "metadata": { + "$ref": "#/components/schemas/IdPMetadata" + } + }, + "required": [ + "id", + "metadata", + "extraInfo" + ], + "type": "object" + }, + "IdPList": { + "properties": { + "providers": { + "items": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + }, + "type": "array" + } + }, + "required": [ + "providers" + ], + "type": "object" + }, + "IdPMetadata": { + "properties": { + "certAuthnResponse": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "issuer": { + "type": "string" + }, + "requestURI": { + "type": "string" + } + }, + "required": [ + "issuer", + "requestURI", + "certAuthnResponse" + ], + "type": "object" + }, + "IdPMetadataInfo": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "value": { + "type": "string" + } + }, + "type": "object" + }, + "Id_AuthnRequest": { + "properties": { + "iD": { + "type": "string" + } + }, + "required": [ + "iD" + ], + "type": "object" + }, + "Invitation": { + "description": "An invitation to join a team on Wire. If invitee is invited from an existing personal account, inviter email is included.", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "url": { + "$ref": "#/components/schemas/URIRef_Absolute" + } + }, + "required": [ + "team", + "id", + "created_at", + "email" + ], + "type": "object" + }, + "InvitationList": { + "description": "A list of sent team invitations.", + "properties": { + "has_more": { + "description": "Indicator that the server has more invitations than returned.", + "type": "boolean" + }, + "invitations": { + "items": { + "$ref": "#/components/schemas/Invitation" + }, + "type": "array" + } + }, + "required": [ + "invitations", + "has_more" + ], + "type": "object" + }, + "InvitationRequest": { + "description": "A request to join a team on Wire.", + "properties": { + "allow_existing": { + "description": "Whether invitations to existing users are allowed.", + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters).", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "InvitationUserView": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "created_by_email": { + "$ref": "#/components/schemas/Email" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "url": { + "$ref": "#/components/schemas/URIRef_Absolute" + } + }, + "required": [ + "team", + "id", + "created_at", + "email" + ], + "type": "object" + }, + "InviteQualified": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "qualified_users": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "JoinConversationByCode": { + "description": "Request body for joining a conversation by code", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "JoinType": { + "enum": [ + "external_add", + "internal_add" + ], + "type": "string" + }, + "KeyPackage": { + "example": "a2V5IHBhY2thZ2UgZGF0YQo=", + "type": "string" + }, + "KeyPackageBundle": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackageBundleEntry" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "KeyPackageBundleEntry": { + "properties": { + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "key_package": { + "$ref": "#/components/schemas/KeyPackage" + }, + "key_package_ref": { + "$ref": "#/components/schemas/KeyPackageRef" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "user", + "client", + "key_package_ref", + "key_package" + ], + "type": "object" + }, + "KeyPackageRef": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "KeyPackageUpload": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackage" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "LHServiceStatus": { + "enum": [ + "configured", + "not_configured", + "disabled" + ], + "type": "string" + }, + "LegalholdConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "LegalholdConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LimitedEventFanoutConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LimitedQualifiedUserIdList_500": { + "properties": { + "qualified_users": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "ListConversations": { + "description": "A request to list some of a user's conversations, including remote ones. Maximum 1000 qualified conversation IDs", + "properties": { + "qualified_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_ids" + ], + "type": "object" + }, + "ListType": { + "description": "true if 'members' doesn't contain all team members", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "ListUsersById": { + "properties": { + "failed": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/components/schemas/UserProfile" + }, + "type": "array" + } + }, + "required": [ + "found" + ], + "type": "object" + }, + "ListUsersQuery": { + "description": "exactly one of qualified_ids or qualified_handles must be provided.", + "example": { + "qualified_ids": [ + { + "domain": "example.com", + "id": "00000000-0000-0000-0000-000000000000" + } + ] + }, + "properties": { + "qualified_handles": { + "items": { + "$ref": "#/components/schemas/Qualified_Handle" + }, + "type": "array" + }, + "qualified_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + } + }, + "type": "object" + }, + "Locale": { + "type": "string" + }, + "LocaleUpdate": { + "properties": { + "locale": { + "$ref": "#/components/schemas/Locale" + } + }, + "required": [ + "locale" + ], + "type": "object" + }, + "LockStatus": { + "enum": [ + "locked", + "unlocked" + ], + "type": "string" + }, + "Login": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "label": { + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "MLSConfig": { + "description": "allowlist of users that may change protocols", + "properties": { + "allowedCipherSuites": { + "items": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "type": "array" + }, + "defaultCipherSuite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "defaultProtocol": { + "$ref": "#/components/schemas/Protocol" + }, + "groupInfoDiagnostics": { + "type": "boolean" + }, + "protocolToggleUsers": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "supportedProtocols": { + "items": { + "$ref": "#/components/schemas/Protocol" + }, + "type": "array" + } + }, + "required": [ + "protocolToggleUsers", + "defaultProtocol", + "allowedCipherSuites", + "defaultCipherSuite", + "supportedProtocols" + ], + "type": "object" + }, + "MLSConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MLSConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "MLSConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MLSConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MLSKeys": { + "properties": { + "ecdsa_secp256r1_sha256": { + "$ref": "#/components/schemas/SomeKey" + }, + "ecdsa_secp384r1_sha384": { + "$ref": "#/components/schemas/SomeKey" + }, + "ecdsa_secp521r1_sha512": { + "$ref": "#/components/schemas/SomeKey" + }, + "ed25519": { + "$ref": "#/components/schemas/SomeKey" + } + }, + "required": [ + "ed25519", + "ecdsa_secp256r1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_secp521r1_sha512" + ], + "type": "object" + }, + "MLSKeysByPurpose": { + "properties": { + "removal": { + "$ref": "#/components/schemas/MLSKeys" + } + }, + "required": [ + "removal" + ], + "type": "object" + }, + "MLSMessage": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "MLSMessageSendingStatus": { + "properties": { + "events": { + "description": "A list of events caused by sending the message.", + "items": { + "$ref": "#/components/schemas/Event" + }, + "type": "array" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "events", + "time" + ], + "type": "object" + }, + "MLSOne2OneConversation_SomeKey": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/OwnConversationV9" + }, + "public_keys": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + }, + "required": [ + "conversation", + "public_keys" + ], + "type": "object" + }, + "MLSPublicKeys": { + "additionalProperties": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "description": "Mapping from signature scheme (tags) to public key data", + "example": { + "ecdsa_secp256r1_sha256": "ZXhhbXBsZQo=", + "ecdsa_secp384r1_sha384": "ZXhhbXBsZQo=", + "ecdsa_secp521r1_sha512": "ZXhhbXBsZQo=", + "ed25519": "ZXhhbXBsZQo=" + }, + "type": "object" + }, + "MLSReset": { + "properties": { + "epoch": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id", + "epoch" + ], + "type": "object" + }, + "ManagedBy": { + "enum": [ + "wire", + "scim" + ], + "type": "string" + }, + "MeetingsConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "MeetingsConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "MeetingsPremiumConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "MeetingsPremiumConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Member": { + "description": "The user ID of the requestor", + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "status": {}, + "status_ref": {}, + "status_time": {} + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "MemberUpdate": { + "properties": { + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "type": "object" + }, + "MemberUpdateData": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_target": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "target": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_target" + ], + "type": "object" + }, + "MembersJoin": { + "properties": { + "add_type": { + "$ref": "#/components/schemas/JoinType" + }, + "user_ids": { + "deprecated": true, + "description": "deprecated", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users", + "add_type" + ], + "type": "object" + }, + "MessageSendingStatus": { + "description": "The Proteus message sending status. It has these fields:\n- `time`: Time of sending message.\n- `missing`: Clients that the message /should/ have been encrypted for, but wasn't.\n- `redundant`: Clients that the message /should not/ have been encrypted for, but was.\n- `deleted`: Clients that were deleted.\n- `failed_to_send`: When message sending fails for some clients but succeeds for others, e.g., because a remote backend is unreachable, this field will contain the list of clients for which the message sending failed. This list should be empty when message sending is not even tried, like when some clients are missing.", + "properties": { + "deleted": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "failed_to_confirm_clients": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "failed_to_send": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "missing": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "redundant": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted", + "failed_to_send", + "failed_to_confirm_clients" + ], + "type": "object" + }, + "MlsE2EIdConfig": { + "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can \"snooze\" this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", + "properties": { + "acmeDiscoveryUrl": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "crlProxy": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "useProxyOnMobile": { + "type": "boolean" + }, + "verificationExpiration": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "verificationExpiration" + ], + "type": "object" + }, + "MlsE2EIdConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsE2EIdConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "MlsE2EIdConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsE2EIdConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MlsMigration": { + "properties": { + "finaliseRegardlessAfter": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "startTime": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + } + }, + "type": "object" + }, + "MlsMigration.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsMigration" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "MlsMigration.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsMigration" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "NameIDFormat": { + "enum": [ + "NameIDFUnspecified", + "NameIDFEmail", + "NameIDFX509", + "NameIDFWindows", + "NameIDFKerberos", + "NameIDFEntity", + "NameIDFPersistent", + "NameIDFTransient" + ], + "type": "string" + }, + "NameIdPolicy": { + "properties": { + "allowCreate": { + "type": "boolean" + }, + "format": { + "$ref": "#/components/schemas/NameIDFormat" + }, + "spNameQualifier": { + "type": "string" + } + }, + "required": [ + "format", + "allowCreate" + ], + "type": "object" + }, + "NewApp": { + "properties": { + "app": { + "$ref": "#/components/schemas/GetApp" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "app", + "password" + ], + "type": "object" + }, + "NewAssetToken": { + "properties": { + "token": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "NewClient": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "cookie": { + "description": "The cookie label, i.e. the label used when logging in.", + "type": "string" + }, + "label": { + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/Prekey" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "password": { + "description": "The password of the authenticated user for verification. Note: Required for registration of the 2nd, 3rd, ... client.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "prekeys": { + "description": "Prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/ClientType" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "prekeys", + "lastkey", + "type" + ], + "type": "object" + }, + "NewConv": { + "description": "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells": { + "type": "boolean" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "message_timer": { + "description": "Per-conversation message timer", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "skip_creator": { + "description": "Don't add creator to the conversation, only works for team admins not wanting to be part of the channels they create.", + "type": "boolean" + }, + "team": { + "$ref": "#/components/schemas/ConvTeamInfo" + }, + "users": { + "deprecated": true, + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewLegalHoldService": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + } + }, + "required": [ + "base_url", + "public_key", + "auth_token" + ], + "type": "object" + }, + "NewOne2OneConv": { + "description": "JSON object to create a new 1:1 conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/ConvTeamInfo" + }, + "users": { + "deprecated": true, + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewPasswordReset": { + "description": "Data to initiate a password reset", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "phone": { + "description": "Email", + "type": "string" + } + }, + "type": "object" + }, + "NewProvider": { + "properties": { + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "name", + "email", + "url", + "description" + ], + "type": "object" + }, + "NewProviderResponse": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "NewService": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "maxItems": 3, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "name", + "summary", + "description", + "base_url", + "public_key", + "assets", + "tags" + ], + "type": "object" + }, + "NewServiceResponse": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "NewTeamCollaborator": { + "properties": { + "permissions": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission" + }, + "type": "array" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + }, + "NewTeamMember": { + "description": "Required data when creating new team members", + "properties": { + "member": { + "description": "the team member to add (the legalhold_status field must be null or missing!)", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + } + }, + "required": [ + "member" + ], + "type": "object" + }, + "NewUser": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_code": { + "$ref": "#/components/schemas/ASCII" + }, + "expires_in": { + "maximum": 604800, + "minimum": 1, + "type": "integer" + }, + "invitation_code": { + "$ref": "#/components/schemas/ASCII" + }, + "label": { + "type": "string" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/BindingNewTeamUser" + }, + "team_code": { + "$ref": "#/components/schemas/ASCII" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "NewUserGroup": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "members" + ], + "type": "object" + }, + "OAuthAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "code": { + "$ref": "#/components/schemas/OAuthAuthorizationCode" + }, + "code_verifier": { + "description": "The code verifier to complete the code challenge", + "maxLength": 128, + "minLength": 43, + "type": "string" + }, + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType" + }, + "redirect_uri": { + "$ref": "#/components/schemas/RedirectUrl" + } + }, + "required": [ + "grant_type", + "client_id", + "code_verifier", + "code", + "redirect_uri" + ], + "type": "object" + }, + "OAuthAccessTokenResponse": { + "properties": { + "access_token": { + "description": "The access token, which has a relatively short lifetime", + "type": "string" + }, + "expires_in": { + "description": "The lifetime of the access token in seconds", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "refresh_token": { + "description": "The refresh token, which has a relatively long lifetime, and can be used to obtain a new access token", + "type": "string" + }, + "token_type": { + "$ref": "#/components/schemas/OAuthAccessTokenType" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "refresh_token" + ], + "type": "object" + }, + "OAuthAccessTokenType": { + "description": "The type of the access token. Currently only `Bearer` is supported.", + "enum": [ + "Bearer" + ], + "type": "string" + }, + "OAuthApplication": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "The OAuth client's name", + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "sessions": { + "description": "The OAuth client's sessions", + "items": { + "$ref": "#/components/schemas/OAuthSession" + }, + "type": "array" + } + }, + "required": [ + "id", + "name", + "sessions" + ], + "type": "object" + }, + "OAuthAuthorizationCode": { + "description": "The authorization code", + "type": "string" + }, + "OAuthClient": { + "properties": { + "application_name": { + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "redirect_url": { + "$ref": "#/components/schemas/RedirectUrl" + } + }, + "required": [ + "client_id", + "application_name", + "redirect_url" + ], + "type": "object" + }, + "OAuthCodeChallenge": { + "description": "Generated by the client from the code verifier (unpadded base64url-encoded SHA256 hash of the code verifier)", + "type": "string" + }, + "OAuthGrantType": { + "description": "Indicates which authorization flow to use. Use `authorization_code` for authorization code flow.", + "enum": [ + "authorization_code", + "refresh_token" + ], + "type": "string" + }, + "OAuthRefreshAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "grant_type", + "client_id", + "refresh_token" + ], + "type": "object" + }, + "OAuthResponseType": { + "description": "Indicates which authorization flow to use. Use `code` for authorization code flow.", + "enum": [ + "code" + ], + "type": "string" + }, + "OAuthRevokeRefreshTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "client_id", + "refresh_token" + ], + "type": "object" + }, + "OAuthSession": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "refresh_token_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "refresh_token_id", + "created_at" + ], + "type": "object" + }, + "Object": { + "additionalProperties": true, + "description": "A single notification event", + "properties": { + "type": { + "description": "Event type", + "type": "string" + } + }, + "title": "Event", + "type": "object" + }, + "OtherMember": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "status": { + "deprecated": true, + "description": "deprecated", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "OtherMemberUpdate": { + "description": "Update user properties of other members relative to a conversation", + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + } + }, + "type": "object" + }, + "OtrMessage": { + "description": "Encrypted message of a conversation", + "properties": { + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "recipient": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + } + }, + "required": [ + "sender", + "recipient", + "text" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "OwnConvMembers": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/components/schemas/OtherMember" + }, + "type": "array" + }, + "self": { + "$ref": "#/components/schemas/Member" + } + }, + "required": [ + "self", + "others" + ], + "type": "object" + }, + "OwnConversation": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "OwnConversationV2": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "OwnConversationV3": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "OwnConversationV6": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "OwnConversationV9": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "OwnKeyPackages": { + "properties": { + "count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "count" + ], + "type": "object" + }, + "PagingState": { + "description": "Paging state that should be supplied to retrieve the next page of results", + "type": "string" + }, + "PasswordChange": { + "properties": { + "new_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "old_password", + "new_password" + ], + "type": "object" + }, + "PasswordReqBody": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "PasswordReset": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "Permissions": { + "description": "This is just a complicated way of representing a team role. self and copy always have to contain the same integer, and only the following integers are allowed: 1025 (partner), 1587 (member), 5951 (admin), 8191 (owner). Unit tests of the galley-types package in wire-server contain an authoritative list.", + "properties": { + "copy": { + "description": "Permissions that this user is able to grant others", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "self": { + "description": "Permissions that the user has", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "self", + "copy" + ], + "type": "object" + }, + "PhoneNumber": { + "description": "A known phone number with a pending password reset.", + "type": "string" + }, + "Pict": { + "items": { + "type": "object" + }, + "maxItems": 10, + "minItems": 0, + "type": "array" + }, + "Prekey": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "PrekeyBundle": { + "properties": { + "clients": { + "items": { + "$ref": "#/components/schemas/ClientPrekey" + }, + "type": "array" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "clients" + ], + "type": "object" + }, + "Priority": { + "enum": [ + "low", + "high" + ], + "type": "string" + }, + "PropertyKeysAndValues": { + "type": "object" + }, + "PropertyValue": { + "description": "An arbitrary JSON value for a property" + }, + "Protocol": { + "enum": [ + "proteus", + "mls", + "mixed" + ], + "type": "string" + }, + "ProtocolUpdate": { + "properties": { + "protocol": { + "$ref": "#/components/schemas/Protocol" + } + }, + "type": "object" + }, + "Provider": { + "properties": { + "description": { + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "id", + "name", + "email", + "url", + "description" + ], + "type": "object" + }, + "ProviderActivationResponse": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "ProviderLogin": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "email", + "password" + ], + "type": "object" + }, + "PubClient": { + "properties": { + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "PublicSubConversation": { + "description": "An MLS subconversation", + "properties": { + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "members": { + "items": { + "$ref": "#/components/schemas/ClientIdentity" + }, + "type": "array" + }, + "parent_qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "subconv_id": { + "type": "string" + } + }, + "required": [ + "parent_qualified_id", + "subconv_id", + "group_id", + "epoch", + "members" + ], + "type": "object" + }, + "PushToken": { + "description": "Native Push Token", + "properties": { + "app": { + "description": "Application", + "type": "string" + }, + "client": { + "description": "Client ID", + "type": "string" + }, + "token": { + "description": "Access Token", + "type": "string" + }, + "transport": { + "$ref": "#/components/schemas/Transport" + } + }, + "required": [ + "transport", + "app", + "token", + "client" + ], + "type": "object" + }, + "PushTokenList": { + "description": "List of Native Push Tokens", + "properties": { + "tokens": { + "description": "Push tokens", + "items": { + "$ref": "#/components/schemas/PushToken" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "QualifiedNewOtrMessage": { + "description": "This object can only be parsed from Protobuf.\nThe specification for the protobuf types is here: \nhttps://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." + }, + "QualifiedUserClientPrekeyMapV4": { + "properties": { + "failed_to_list": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "qualified_user_client_prekeys": { + "additionalProperties": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + }, + "type": "object" + } + }, + "required": [ + "qualified_user_client_prekeys" + ], + "type": "object" + }, + "QualifiedUserClients": { + "additionalProperties": { + "additionalProperties": { + "items": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "type": "array" + }, + "type": "object" + }, + "description": "Map of Domain to UserClients", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + } + }, + "type": "object" + }, + "QualifiedUserIdList_with_EdMemberLeftReason": { + "properties": { + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "reason", + "qualified_user_ids", + "user_ids" + ], + "type": "object" + }, + "QualifiedUserMap_Set_PubClient": { + "additionalProperties": { + "$ref": "#/components/schemas/UserMap_Set_PubClient" + }, + "description": "Map of Domain to (UserMap (Set_PubClient)).", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + } + }, + "type": "object" + }, + "Qualified_ConvId": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "Qualified_Handle": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + } + }, + "required": [ + "domain", + "handle" + ], + "type": "object" + }, + "Qualified_UserId": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "QueuedNotification": { + "description": "A single notification", + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "payload": { + "description": "List of events", + "items": { + "$ref": "#/components/schemas/Object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "id", + "payload" + ], + "type": "object" + }, + "QueuedNotificationList": { + "description": "Zero or more notifications", + "properties": { + "has_more": { + "description": "Whether there are still more notifications.", + "type": "boolean" + }, + "notifications": { + "description": "Notifications", + "items": { + "$ref": "#/components/schemas/QueuedNotification" + }, + "type": "array" + }, + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "notifications" + ], + "type": "object" + }, + "RTCConfiguration": { + "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", + "properties": { + "ice_servers": { + "description": "Array of 'RTCIceServer' objects", + "items": { + "$ref": "#/components/schemas/RTCIceServer" + }, + "minItems": 1, + "type": "array" + }, + "is_federating": { + "description": "True if the client should connect to an SFT in the sft_servers_all and request it to federate", + "type": "boolean" + }, + "sft_servers": { + "description": "Array of 'SFTServer' objects (optional)", + "items": { + "$ref": "#/components/schemas/SftServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers_all": { + "description": "Array of all SFT servers", + "items": { + "$ref": "#/components/schemas/SftServer" + }, + "type": "array" + }, + "ttl": { + "description": "Number of seconds after which the configuration should be refreshed (advisory)", + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "ice_servers", + "ttl" + ], + "type": "object" + }, + "RTCIceServer": { + "description": "A subset of the WebRTC 'RTCIceServer' object", + "properties": { + "credential": { + "$ref": "#/components/schemas/ASCII" + }, + "urls": { + "description": "Array of TURN server addresses of the form 'turn::'", + "items": { + "$ref": "#/components/schemas/TurnURI" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "$ref": "#/components/schemas/TurnUsername" + } + }, + "required": [ + "urls", + "username", + "credential" + ], + "type": "object" + }, + "RedirectUrl": { + "description": "The URL must match the URL that was used to generate the authorization code.", + "type": "string" + }, + "RefreshAppCookieResponse": { + "properties": { + "cookie": { + "$ref": "#/components/schemas/SomeUserToken" + } + }, + "required": [ + "cookie" + ], + "type": "object" + }, + "RegisteredDomains": { + "properties": { + "registered_domains": { + "items": { + "$ref": "#/components/schemas/DomainRegistrationResponse" + }, + "type": "array" + } + }, + "required": [ + "registered_domains" + ], + "type": "object" + }, + "Relation": { + "enum": [ + "accepted", + "blocked", + "pending", + "ignored", + "sent", + "cancelled", + "missing-legalhold-consent" + ], + "type": "string" + }, + "RemoveBotResponse": { + "properties": { + "event": { + "$ref": "#/components/schemas/Event" + } + }, + "required": [ + "event" + ], + "type": "object" + }, + "RemoveCookies": { + "description": "Data required to remove cookies", + "properties": { + "ids": { + "description": "A list of cookie IDs to revoke", + "items": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "labels": { + "description": "A list of cookie labels for which to revoke the cookies", + "items": { + "type": "string" + }, + "type": "array" + }, + "password": { + "description": "The user's password", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "RemoveLegalHoldSettingsRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RichField": { + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + "RichInfoAssocList": { + "description": "json object with case-insensitive fields.", + "properties": { + "fields": { + "items": { + "$ref": "#/components/schemas/RichField" + }, + "type": "array" + }, + "version": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "version", + "fields" + ], + "type": "object" + }, + "Role": { + "description": "Role of the invited user", + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "RoleName": { + "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", + "type": "string" + }, + "SFTUsername": { + "description": "String containing the SFT username", + "type": "string" + }, + "SSOConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ScimTokenInfo": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTime" + }, + "description": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "idp": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "id", + "created_at", + "description", + "name" + ], + "type": "object" + }, + "ScimTokenList": { + "properties": { + "tokens": { + "items": { + "$ref": "#/components/schemas/ScimTokenInfo" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "ScimTokenName": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SearchResult_Contact": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/components/schemas/Contact" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "search_policy": { + "$ref": "#/components/schemas/FederatedUserSearchPolicy" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SearchResult_TeamContact": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/components/schemas/TeamContact" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "search_policy": { + "$ref": "#/components/schemas/FederatedUserSearchPolicy" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig": { + "properties": { + "enforcedTimeoutSeconds": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforcedTimeoutSeconds" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "SendActivationCode": { + "description": "Data for requesting an email code to be sent. 'email' must be present.", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "SendVerificationCode": { + "properties": { + "action": { + "$ref": "#/components/schemas/VerificationAction" + }, + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "action", + "email" + ], + "type": "object" + }, + "ServerTime": { + "description": "The current server time", + "properties": { + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "Service": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "auth_tokens": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "minItems": 1, + "type": "array" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "public_keys": { + "items": { + "$ref": "#/components/schemas/ServiceKey" + }, + "minItems": 1, + "type": "array" + }, + "summary": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "type": "array" + } + }, + "required": [ + "id", + "name", + "summary", + "description", + "base_url", + "auth_tokens", + "public_keys", + "assets", + "tags", + "enabled" + ], + "type": "object" + }, + "ServiceKey": { + "properties": { + "pem": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "size": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "type": { + "$ref": "#/components/schemas/ServiceKeyType" + } + }, + "required": [ + "type", + "size", + "pem" + ], + "type": "object" + }, + "ServiceKeyPEM": { + "example": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0\nG06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH\nWvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV\nVPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS\nbUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8\n7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la\nnQIDAQAB\n-----END PUBLIC KEY-----\n", + "type": "string" + }, + "ServiceKeyType": { + "enum": [ + "rsa" + ], + "type": "string" + }, + "ServiceProfile": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "summary": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "type": "array" + } + }, + "required": [ + "id", + "provider", + "name", + "summary", + "description", + "assets", + "tags", + "enabled" + ], + "type": "object" + }, + "ServiceProfilePage": { + "properties": { + "has_more": { + "type": "boolean" + }, + "services": { + "items": { + "$ref": "#/components/schemas/ServiceProfile" + }, + "type": "array" + } + }, + "required": [ + "has_more", + "services" + ], + "type": "object" + }, + "ServiceRef": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "provider" + ], + "type": "object" + }, + "ServiceTag": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], + "type": "string" + }, + "ServiceTagList": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "type": "array" + }, + "SetSearchable": { + "properties": { + "set_searchable": { + "type": "boolean" + } + }, + "required": [ + "set_searchable" + ], + "type": "object" + }, + "SftServer": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", + "items": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "type": "array" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "SimpleMember": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "SimplifiedUserConnectionRequestQRCode.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SomeKey": {}, + "SomeUserToken": { + "type": "string" + }, + "Sso": { + "properties": { + "issuer": { + "type": "string" + }, + "nameid": { + "type": "string" + } + }, + "required": [ + "issuer", + "nameid" + ], + "type": "object" + }, + "SsoSettings": { + "properties": { + "default_sso_code": { + "$ref": "#/components/schemas/UUID" + } + }, + "type": "object" + }, + "StealthUsersConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SupportedProtocolUpdate": { + "properties": { + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + } + }, + "required": [ + "supported_protocols" + ], + "type": "object" + }, + "SystemSettings": { + "properties": { + "setEnableMls": { + "description": "Whether MLS is enabled or not", + "type": "boolean" + }, + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation", + "setEnableMls" + ], + "type": "object" + }, + "SystemSettingsPublic": { + "properties": { + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation" + ], + "type": "object" + }, + "Team": { + "description": "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`.", + "properties": { + "binding": { + "$ref": "#/components/schemas/TeamBinding" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "required": [ + "id", + "creator", + "name", + "icon" + ], + "type": "object" + }, + "TeamBinding": { + "deprecated": true, + "description": "Deprecated, please ignore.", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "TeamCollaborator": { + "properties": { + "permissions": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "team", + "permissions" + ], + "type": "object" + }, + "TeamContact": { + "properties": { + "accent_id": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_unvalidated": { + "$ref": "#/components/schemas/Email" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + }, + "saml_idp": { + "type": "string" + }, + "scim_external_id": { + "type": "string" + }, + "searchable": { + "type": "boolean" + }, + "sso": { + "$ref": "#/components/schemas/Sso" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "user_groups": { + "description": "List of user group ids the user is a member of", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "id", + "name", + "user_groups", + "searchable" + ], + "type": "object" + }, + "TeamConversation": { + "description": "Team conversation data", + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + } + }, + "required": [ + "conversation", + "managed" + ], + "type": "object" + }, + "TeamConversationList": { + "description": "Team conversation list", + "properties": { + "conversations": { + "items": { + "$ref": "#/components/schemas/TeamConversation" + }, + "type": "array" + } + }, + "required": [ + "conversations" + ], + "type": "object" + }, + "TeamDeleteData": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "type": "object" + }, + "TeamDomainRedirectTag": { + "enum": [ + "no-registration", + "none" + ], + "type": "string" + }, + "TeamInvite Tag": { + "enum": [ + "allowed", + "not-allowed", + "team" + ], + "type": "string" + }, + "TeamInviteConfig": { + "properties": { + "domain_redirect": { + "$ref": "#/components/schemas/TeamDomainRedirectTag" + }, + "sso": { + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "team_invite": { + "$ref": "#/components/schemas/TeamInvite Tag" + } + }, + "required": [ + "team_invite", + "team" + ], + "type": "object" + }, + "TeamMember": { + "description": "team member data", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user" + ], + "type": "object" + }, + "TeamMemberDeleteData": { + "description": "Data for a team member deletion request in case of binding teams.", + "properties": { + "password": { + "description": "The account password to authorise the deletion.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "TeamMemberList": { + "description": "list of team member", + "properties": { + "hasMore": { + "$ref": "#/components/schemas/ListType" + }, + "members": { + "description": "the array of team members", + "items": { + "$ref": "#/components/schemas/TeamMember" + }, + "type": "array" + } + }, + "required": [ + "members", + "hasMore" + ], + "type": "object" + }, + "TeamMembersPage": { + "properties": { + "hasMore": { + "type": "boolean" + }, + "members": { + "items": { + "$ref": "#/components/schemas/TeamMember" + }, + "type": "array" + }, + "pagingState": { + "$ref": "#/components/schemas/TeamMembers_PagingState" + } + }, + "required": [ + "members", + "hasMore", + "pagingState" + ], + "type": "object" + }, + "TeamMembers_PagingState": { + "type": "string" + }, + "TeamSearchVisibility": { + "description": "value of visibility", + "enum": [ + "standard", + "no-name-outside-team" + ], + "type": "string" + }, + "TeamSearchVisibilityView": { + "description": "Search visibility value for the team", + "properties": { + "search_visibility": { + "$ref": "#/components/schemas/TeamSearchVisibility" + } + }, + "required": [ + "search_visibility" + ], + "type": "object" + }, + "TeamSize": { + "description": "A simple object with a total number of team members.", + "properties": { + "teamSize": { + "description": "Team size.", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "teamSize" + ], + "type": "object" + }, + "TeamUpdateData": { + "properties": { + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "type": "object" + }, + "Time": { + "properties": { + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "Token": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "TokenType": { + "enum": [ + "Bearer" + ], + "type": "string" + }, + "Transport": { + "description": "Transport", + "enum": [ + "GCM", + "APNS", + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" + ], + "type": "string" + }, + "TurnURI": { + "type": "string" + }, + "TurnUsername": { + "description": "Username to use for authenticating against the given TURN servers", + "type": "string" + }, + "TypingData": { + "properties": { + "status": { + "$ref": "#/components/schemas/TypingStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "TypingStatus": { + "enum": [ + "started", + "stopped" + ], + "type": "string" + }, + "URIRef_Absolute": { + "description": "URL of the invitation link to be sent to the invitee", + "type": "string" + }, + "UTCTime": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "UTCTimeMillis": { + "description": "The time when the session was created", + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqqZ", + "type": "string" + }, + "UUID": { + "description": "The OAuth client's ID", + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "Unnamed": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + }, + "UpdateBotPrekeys": { + "properties": { + "prekeys": { + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + } + }, + "required": [ + "prekeys" + ], + "type": "object" + }, + "UpdateClient": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "label": { + "description": "A new name for this client.", + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/Prekey" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "prekeys": { + "description": "New prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + } + }, + "type": "object" + }, + "UpdateProvider": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "type": "object" + }, + "UpdateService": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "maxItems": 3, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "UpdateServiceConn": { + "properties": { + "auth_tokens": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "enabled": { + "type": "boolean" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "public_keys": { + "items": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "UpdateServiceWhitelist": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "whitelisted": { + "type": "boolean" + } + }, + "required": [ + "provider", + "id", + "whitelisted" + ], + "type": "object" + }, + "UpdateUserGroupChannels": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "channels" + ], + "type": "object" + }, + "UpdateUserGroupMembers": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "members" + ], + "type": "object" + }, + "User": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_unvalidated": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "searchable": { + "type": "boolean" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "status": { + "$ref": "#/components/schemas/AccountStatus" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "status", + "locale" + ], + "type": "object" + }, + "UserAsset": { + "properties": { + "key": { + "$ref": "#/components/schemas/AssetKey" + }, + "size": { + "$ref": "#/components/schemas/AssetSize" + }, + "type": { + "$ref": "#/components/schemas/AssetType" + } + }, + "required": [ + "key", + "type" + ], + "type": "object" + }, + "UserClientMap": { + "additionalProperties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "object" + }, + "UserClientPrekeyMap": { + "additionalProperties": { + "additionalProperties": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "type": "object" + }, + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": { + "44901fb0712e588f": { + "id": 1, + "key": "pQABAQECoQBYIOjl7hw0D8YRNq..." + } + } + }, + "type": "object" + }, + "UserClients": { + "additionalProperties": { + "items": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "type": "array" + }, + "description": "Map of user id to list of client ids.", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + }, + "type": "object" + }, + "UserConnection": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "last_update": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_to": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "status": { + "$ref": "#/components/schemas/Relation" + }, + "to": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "from", + "qualified_to", + "status", + "last_update" + ], + "type": "object" + }, + "UserGroup": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + }, + "channelsCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "createdAt": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managedBy": { + "$ref": "#/components/schemas/ManagedBy" + }, + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "membersCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "name", + "members", + "managedBy", + "createdAt" + ], + "type": "object" + }, + "UserGroupAddUsers": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "members" + ], + "type": "object" + }, + "UserGroupMeta": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + }, + "channelsCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "createdAt": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managedBy": { + "$ref": "#/components/schemas/ManagedBy" + }, + "membersCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "name", + "managedBy", + "createdAt" + ], + "type": "object" + }, + "UserGroupNameAvailability": { + "properties": { + "name_available": { + "type": "boolean" + } + }, + "required": [ + "name_available" + ], + "type": "object" + }, + "UserGroupPage": { + "description": "This is the last page if it contains fewer rows than requested. There may be 0 rows on a page.", + "properties": { + "page": { + "items": { + "$ref": "#/components/schemas/UserGroupMeta" + }, + "type": "array" + }, + "total": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "page", + "total" + ], + "type": "object" + }, + "UserGroupUpdate": { + "properties": { + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "UserIdList": { + "properties": { + "user_ids": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "user_ids" + ], + "type": "object" + }, + "UserLegalHoldStatus": { + "description": "The state of Legal Hold compliance for the member", + "enum": [ + "enabled", + "pending", + "disabled", + "no_consent" + ], + "type": "string" + }, + "UserLegalHoldStatusResponse": { + "properties": { + "client": { + "$ref": "#/components/schemas/Id" + }, + "last_prekey": { + "$ref": "#/components/schemas/Prekey" + }, + "status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "UserMap_Set_PubClient": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array", + "uniqueItems": true + }, + "description": "Map of UserId to (Set PubClient)", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + }, + "type": "object" + }, + "UserProfile": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "searchable": { + "type": "boolean" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/UserType" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "legalhold_status" + ], + "type": "object" + }, + "UserSSOId": { + "properties": { + "scim_external_id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "tenant": { + "type": "string" + } + }, + "type": "object" + }, + "UserType": { + "enum": [ + "regular", + "app", + "bot" + ], + "type": "string" + }, + "UserUpdate": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "ValidateSAMLEmailsConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "VerificationAction": { + "enum": [ + "create_scim_token", + "login", + "delete_team" + ], + "type": "string" + }, + "VerifyDeleteUser": { + "description": "Data for verifying an account deletion.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "VersionInfo": { + "example": { + "development": [ + 14 + ], + "domain": "example.com", + "federation": false, + "supported": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 + ] + }, + "properties": { + "development": { + "items": { + "$ref": "#/components/schemas/VersionNumber" + }, + "type": "array" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "federation": { + "type": "boolean" + }, + "supported": { + "items": { + "$ref": "#/components/schemas/VersionNumber" + }, + "type": "array" + } + }, + "required": [ + "supported", + "development", + "federation", + "domain" + ], + "type": "object" + }, + "VersionNumber": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 + ], + "type": "integer" + }, + "ViewLegalHoldService": { + "properties": { + "settings": { + "$ref": "#/components/schemas/ViewLegalHoldServiceInfo" + }, + "status": { + "$ref": "#/components/schemas/LHServiceStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ViewLegalHoldServiceInfo": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "fingerprint": { + "$ref": "#/components/schemas/Fingerprint" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team_id", + "base_url", + "fingerprint", + "auth_token", + "public_key" + ], + "type": "object" + }, + "WireIdP": { + "properties": { + "apiVersion": { + "$ref": "#/components/schemas/WireIdPAPIVersion" + }, + "domain": { + "type": "string" + }, + "handle": { + "type": "string" + }, + "oldIssuers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "replacedBy": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "oldIssuers", + "handle" + ], + "type": "object" + }, + "WireIdPAPIVersion": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "backend_config": { + "properties": { + "config_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_url", + "webapp_url" + ], + "type": "object" + }, + "new-otr-message": { + "properties": { + "data": { + "type": "string" + }, + "native_priority": { + "$ref": "#/components/schemas/Priority" + }, + "native_push": { + "type": "boolean" + }, + "recipients": { + "$ref": "#/components/schemas/UserClientMap" + }, + "report_missing": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "transient": { + "type": "boolean" + } + }, + "required": [ + "sender", + "recipients" + ], + "type": "object" + } + }, + "securitySchemes": { + "ZAuth": { + "description": "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'.", + "in": "header", + "name": "Authorization", + "type": "apiKey" + } + } + }, + "info": { + "description": "## Authentication / Authorization\n\nThe end-points in this API support differing authorization protocols:\nsome are unauthenticated (`/api-version`, `/login`), some require\n[zauth](), and some support both [zauth]() and [oauth]().\n\nThe end-points that require zauth are labelled so in the description\nbelow. The end-points that support oauth as an alternative to zauth\nhave the required oauth scopes listed in the same description.\n\nFuther reading:\n- https://docs.wire.com/developer/reference/oauth.html\n- https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/src/Wire/API/Routes/Public.hs (search for HasSwagger instances)\n- `curl https://staging-nginz-https.zinfra.io/v4/api/swagger.json | jq '.security, .securityDefinitions`\n\n### SSO Endpoints\n\n#### Overview\n\n`/sso/metadata` will be requested by the IdPs to learn how to talk to wire.\n\n`/sso/initiate-login`, `/sso/finalize-login` are for the SAML authentication handshake performed by a user in order to log into wire. They are not exactly standard in their details: they may return HTML or XML; redirect to error URLs instead of throwing errors, etc.\n\n`/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json.\n\n\n#### Configuring IdPs\n\nIdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.)\n\n##### okta.com\n\nOkta will ask you to provide two URLs when you set it up for talking to wireapp:\n\n1. The `Single sign on URL`. This is the end-point that accepts the user's credentials after successful authentication against the IdP. Choose `/sso/finalize-login` with schema and hostname of the wire server you are configuring.\n\n2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element.\n\n##### centrify.com\n\nCentrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections.\n\n## Federation errors\n\nEndpoints involving federated calls to other domains can return some extra failure responses, common to all endpoints. Instead of listing them as possible responses for each endpoint, we document them here.\n\nFor errors that are more likely to be transient, we suggest clients to retry whatever request resulted in the error. Transient errors are indicated explicitly below.\n\n**Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields:\n\n - `type`: \"federation\" (just the literal string in quotes, which can be used as an error type identifier when parsing errors)\n - `domain`: the target backend of the RPC that failed;\n - `path`: the path of the RPC that failed.\n\n### Domain errors\n\nErrors in this category result from trying to communicate with a backend that is considered non-existent or invalid. They can result from invalid user input or client issues, but they can also be a symptom of misconfiguration in one or multiple backends. These errors have a 4xx status code.\n\n - **Remote backend not found** (status: 422, label: `invalid-domain`): This backend attempted to contact a backend which does not exist or is not properly configured. For the most part, clients can consider this error equivalent to a domain not existing, although it should be noted that certain mistakes in the DNS configuration on a remote backend can lead to the backend not being recognized, and hence to this error. It is therefore not advisable to take any destructive action upon encountering this error, such as deleting remote users from conversations.\n - **Federation denied locally** (status: 400, label: `federation-denied`): This backend attempted an RPC to a non-whitelisted backend. Similar considerations as for the previous error apply.\n - **Federation not enabled** (status: 400, label: `federation-not-enabled`): Federation has not been configured for this backend. This will happen if a federation-aware client tries to talk to a backend for which federation is disabled, or if federation was disabled on the backend after reaching a federation-specific state (e.g. conversations with remote users). There is no way to cleanly recover from these errors at this point.\n\n### Local federation errors\n\nAn error in this category likely indicates an issue with the configuration of federation on the local backend. Possibly transient errors are indicated explicitly below. All these errors have a 500 status code.\n\n - **Federation unavailable** (status: 500, label: `federation-not-available`): Federation is configured for this backend, but the local federator cannot be reached. This can be transient, so clients should retry the request.\n - **Federation not implemented** (status: 422, label: `federation-not-implemented`): Federated behaviour for a certain endpoint is not yet implemented.\n - **Federator discovery failed** (status: 400, label: `discovery-failure`): A DNS error occurred during discovery of a remote backend. This can be transient, so clients should retry the request.\n - **Local federation error** (status: 500, label: `federation-local-error`): An error occurred in the communication between this backend and its local federator. These errors are most likely caused by bugs in the backend, and should be reported as such.\n\n### Remote federation errors\n\nErrors in this category are returned in case of communication issues between the local backend and a remote one, or if the remote side encountered an error while processing an RPC. Some errors in this category might be caused by incorrect client behaviour, wrong user input, or incorrect certificate configuration. Possibly transient errors are indicated explicitly. We use non-standard 5xx status codes for these errors.\n\n - **HTTP2 error** (status: 533, label: `federation-http2-error`): The current federator encountered an error when making an HTTP2 request to a remote one. Check the error message for more details.\n - **Connection refused** (status: 521, label: `federation-connection-refused`): The local federator could not connect to a remote one. This could be transient, so clients should retry the request.\n - **TLS failure**: (status: 525, label: `federation-tls-error`): An error occurred during the TLS handshake between the local federator and a remote one. This is most likely due to an issue with the certificate on the remote end.\n - **Remote federation error** (status: 533, label: `federation-remote-error`): The remote backend could not process a request coming from this backend. Check the error message for more details.\n - **Version negotiation error** (status: 533, label: `federation-version-error`): The remote backend returned invalid version information.\n\n### Backend compatibility errors\n\nAn error in this category will be returned when this backend makes an invalid or unsupported RPC to another backend. This can indicate some incompatibility between backends or a backend bug. These errors are unlikely to be transient, so retrying requests is *not* advised.\n\n - **Version mismatch** (status: 531, label: `federation-version-mismatch`): A remote backend is running an unsupported version of the federator.\n - **Invalid content type** (status: 533, label: `federation-invalid-content-type`): An RPC to another backend returned with an invalid content type.\n - **Unsupported content type** (status: 533, label: `federation-unsupported-content-type`): An RPC to another backend returned with an unsupported content type.\n", + "title": "Wire-Server API", + "version": "" + }, + "openapi": "3.0.0", + "paths": { + "/access": { + "post": { + "description": " [internal route ID: \"access\"]\n\nYou can provide only a cookie or a cookie and token. Every other combination is invalid. Access tokens can be given as query parameter or authorisation header, with the latter being preferred.", + "operationId": "access", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Obtain an access tokens for a cookie" + } + }, + "/access/logout": { + "post": { + "description": " [internal route ID: \"logout\"]\n\nCalling this endpoint will effectively revoke the given cookie and subsequent calls to /access with the same cookie will result in a 403.", + "operationId": "logout", + "responses": { + "200": { + "description": "Logout" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Log out in order to remove a cookie from the server" + } + }, + "/access/self/email": { + "put": { + "description": " [internal route ID: \"change-self-email\"]\n\n", + "operationId": "change-self-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + } + } + }, + "required": true + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Update accepted and pending activation of the new email" + }, + "204": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "No update, current and new email address are the same\n\nEmail address activated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid e-mail address. (label: `invalid-email`) or `body`" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Change your email address" + } + }, + "/activate": { + "get": { + "description": " [internal route ID: \"get-activate\"]\n\nSee also 'POST /activate' which has a larger feature set.", + "operationId": "get-activate", + "parameters": [ + { + "description": "Activation key", + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Activation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `code` or `key`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + }, + "post": { + "description": " [internal route ID: \"post-activate\"]\n\nActivation only succeeds once and the number of failed attempts for a valid key is limited.", + "operationId": "post-activate", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Activate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + } + }, + "/activate/send": { + "post": { + "description": " [internal route ID: \"post-activate-send\"]\n\n", + "operationId": "post-activate-send", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendActivationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Activation code sent." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "blacklisted-email", + "message": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + }, + "451": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 451, + "label": "domain-blocked-for-registration", + "message": "[Customer extension] The email domain has been blocked for Wire users. Please contact your IT department." + }, + "properties": { + "code": { + "enum": [ + 451 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-blocked-for-registration" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "[Customer extension] The email domain has been blocked for Wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)" + } + }, + "summary": "Send (or resend) an email activation code." + } + }, + "/api-version": { + "get": { + "description": " [internal route ID: \"get-version\"]\n\n", + "operationId": "get-version", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VersionInfo" + } + } + }, + "description": "" + } + } + } + }, + "/assets": { + "post": { + "description": " [internal route ID: \"assets-upload\"]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/assets/{key_domain}/{key}": { + "delete": { + "description": " [internal route ID: \"assets-delete\"]\n\n**Note**: only local assets can be deleted.", + "operationId": "assets-delete", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key_domain` or `key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: \"assets-download\"]\n\n**Note**: local assets result in a redirect, while remote assets are streamed directly.", + "operationId": "assets-download", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset returned directly with content type `application/octet-stream`" + }, + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key_domain` or `key` or Asset not found (label: `not-found`)\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/assets/{key}/token": { + "delete": { + "description": " [internal route ID: \"tokens-delete\"]\n\n**Note**: deleting the token makes the asset public.", + "operationId": "tokens-delete", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset token deleted" + } + }, + "summary": "Delete an asset token" + }, + "post": { + "description": " [internal route ID: \"tokens-renew\"]\n\n", + "operationId": "tokens-renew", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewAssetToken" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Renew an asset token" + } + }, + "/await": { + "get": { + "description": " [internal route ID: \"await-notifications\"]\n\n", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "await-notifications", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } + }, + "/bot/assets": { + "post": { + "description": " [internal route ID: (\"assets-upload-v3\", bot)]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload-v3_bot", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/bot/assets/{key}": { + "delete": { + "description": " [internal route ID: (\"assets-delete-v3\", bot)]\n\n", + "operationId": "assets-delete-v3_bot", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: (\"assets-download-v3\", bot)]\n\n", + "operationId": "assets-download-v3_bot", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/bot/client": { + "get": { + "description": " [internal route ID: \"bot-get-client\"]\n\n", + "operationId": "bot-get-client", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client" + } + } + }, + "description": "Client found" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)\n\nClient not found (label: `client-not-found`)" + } + }, + "summary": "Get client for bot" + } + }, + "/bot/client/prekeys": { + "get": { + "description": " [internal route ID: \"bot-list-prekeys\"]\n\n", + "operationId": "bot-list-prekeys", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List prekeys for bot" + }, + "post": { + "description": " [internal route ID: \"bot-update-prekeys\"]\n\n", + "operationId": "bot-update-prekeys", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateBotPrekeys" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)" + } + }, + "summary": "Update prekeys for bot" + } + }, + "/bot/conversation": { + "get": { + "description": " [internal route ID: \"get-bot-conversation\"]\n\n", + "operationId": "get-bot-conversation", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BotConvView" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + } + } + } + }, + "/bot/conversations/{conv}": { + "post": { + "description": " [internal route ID: \"add-bot\"]\n\n", + "operationId": "add-bot", + "parameters": [ + { + "in": "path", + "name": "conv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBot" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "service-disabled", + "message": "The desired service is currently disabled." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "service-disabled", + "too-many-members", + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The desired service is currently disabled. (label: `service-disabled`)\n\nMaximum number of members per conversation reached. (label: `too-many-members`)\n\nThe operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Add bot" + } + }, + "/bot/conversations/{conv}/{bot}": { + "delete": { + "description": " [internal route ID: \"remove-bot\"]\n\n", + "operationId": "remove-bot", + "parameters": [ + { + "in": "path", + "name": "conv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "bot", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse" + } + } + }, + "description": "User found" + }, + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation", + "message": "The operation is not allowed in this conversation." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Remove bot" + } + }, + "/bot/messages": { + "post": { + "description": " [internal route ID: \"post-bot-message-unqualified\"]\n\n", + "operationId": "post-bot-message-unqualified", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Missing clients" + } + } + } + }, + "/bot/self": { + "delete": { + "description": " [internal route ID: \"bot-delete-self\"]\n\n", + "operationId": "bot-delete-self", + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-bot", + "message": "The targeted user is not a bot." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-bot", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The targeted user is not a bot. (label: `invalid-bot`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Delete self" + }, + "get": { + "description": " [internal route ID: \"bot-get-self\"]\n\n", + "operationId": "bot-get-self", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserProfile" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "User not found (label: `not-found`)" + } + }, + "summary": "Get self" + } + }, + "/bot/users": { + "get": { + "description": " [internal route ID: \"bot-list-users\"]\n\n", + "operationId": "bot-list-users", + "parameters": [ + { + "in": "query", + "name": "ids", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/BotUserView" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List users" + } + }, + "/bot/users/prekeys": { + "post": { + "description": " [internal route ID: \"bot-claim-users-prekeys\"]\n\n", + "operationId": "bot-claim-users-prekeys", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClients" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients", + "too-many-clients", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nToo many clients (label: `too-many-clients`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Claim users prekeys" + } + }, + "/bot/users/{user}/clients": { + "get": { + "description": " [internal route ID: \"bot-get-user-clients\"]\n\n", + "operationId": "bot-get-user-clients", + "parameters": [ + { + "in": "path", + "name": "user", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get user clients" + } + }, + "/broadcast/otr/messages": { + "post": { + "description": " [internal route ID: \"post-otr-broadcast-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-otr-broadcast-unqualified", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + }, + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `report_missing` or `ignore_missing`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + } + }, + "/broadcast/proteus/messages": { + "post": { + "description": " [internal route ID: \"post-proteus-broadcast\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-proteus-broadcast", + "requestBody": { + "content": { + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/QualifiedNewOtrMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + } + }, + "/calls/config/v2": { + "get": { + "description": " [internal route ID: \"get-calls-config-v2\"]\n\n", + "operationId": "get-calls-config-v2", + "parameters": [ + { + "description": "Limit resulting list. Allowed values [1..10]", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RTCConfiguration" + } + } + }, + "description": "" + } + }, + "summary": "Retrieve all TURN server addresses and credentials. Clients are expected to do a DNS lookup to resolve the IP addresses of the given hostnames " + } + }, + "/clients": { + "get": { + "description": " [internal route ID: \"list-clients\"]\n\n", + "operationId": "list-clients", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Client" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Client" + }, + "type": "array" + } + } + }, + "description": "List of clients" + } + }, + "summary": "List the registered clients" + }, + "post": { + "description": " [internal route ID: \"add-client\"]\n\n", + "operationId": "add-client", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewClient" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client" + } + } + }, + "description": "Client registered", + "headers": { + "Location": { + "description": "Client ID", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "missing-auth", + "too-many-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nToo many clients (label: `too-many-clients`)" + } + }, + "summary": "Register a new client" + } + }, + "/clients/{cid}/access-token": { + "post": { + "description": " [internal route ID: \"create-access-token\"]\n\nCreate an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned as JWT DPoP token in the `DPoP` header.", + "operationId": "create-access-token", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "cid", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "DPoP", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse" + } + } + }, + "description": "Access token created", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Create a JWT DPoP access token" + } + }, + "/clients/{client}": { + "delete": { + "description": " [internal route ID: \"delete-client\"]\n\n", + "operationId": "delete-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteClient" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client deleted" + } + }, + "summary": "Delete an existing client" + }, + "get": { + "description": " [internal route ID: \"get-client\"]\n\n", + "operationId": "get-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client" + } + } + }, + "description": "Client found" + }, + "404": { + "description": "`client` or Client not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a registered client by ID" + }, + "put": { + "description": " [internal route ID: \"update-client\"]\n\n", + "operationId": "update-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateClient" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + } + }, + "summary": "Update a registered client" + } + }, + "/clients/{client}/capabilities": { + "get": { + "description": " [internal route ID: \"get-client-capabilities\"]\n\n", + "operationId": "get-client-capabilities", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientCapabilityList" + } + } + }, + "description": "" + } + }, + "summary": "Read back what the client has been posting about itself" + } + }, + "/clients/{client}/nonce": { + "get": { + "description": " [internal route ID: \"get-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "operationId": "get-nonce", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + }, + "head": { + "description": " [internal route ID: \"head-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "operationId": "head-nonce", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + } + }, + "/clients/{client}/prekeys": { + "get": { + "description": " [internal route ID: \"get-client-prekeys\"]\n\n", + "operationId": "get-client-prekeys", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "List the remaining prekey IDs of a client" + } + }, + "/connections/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-connection\"]\n\n", + "operationId": "get-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection found" + }, + "404": { + "description": "`uid_domain` or `uid` or Connection not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get an existing connection to another user (local or remote)" + }, + "post": { + "description": " [internal route ID: \"create-connection\"]\n\nYou can have no more than 1000 connections in accepted or sent state", + "operationId": "create-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection existed" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection was created" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (label: `no-identity`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Create a connection to another user" + }, + "put": { + "description": " [internal route ID: \"update-connection\"]\n\n", + "operationId": "update-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConnectionUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection updated" + }, + "204": { + "description": "Connection unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "bad-conn-update", + "not-connected", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (label: `no-identity`)\n\nInvalid status transition (label: `bad-conn-update`)\n\nUsers are not connected (label: `not-connected`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Update a connection to another user" + } + }, + "/conversations": { + "post": { + "description": " [internal route ID: \"create-group-conversation\"]\n\nThis returns 201 when a new conversation is created, and 200 when the conversation already existed", + "operationId": "create-group-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewConv" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversation" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled", + "non-empty-member-list" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "channels-not-enabled", + "message": "The channels feature is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "channels-not-enabled", + "not-mls-conversation", + "missing-legalhold-consent", + "operation-denied", + "no-team-member", + "not-connected", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The channels feature is not enabled for this team (label: `channels-not-enabled`)\n\nThis operation requires an MLS conversation (label: `not-mls-conversation`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nConversation access denied (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a new conversation" + } + }, + "/conversations/code-check": { + "post": { + "description": " [internal route ID: \"code-check\"]\n\nIf the guest links team feature is disabled, this will fail with 404 CodeNotFound.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled.", + "operationId": "code-check", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Valid" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation-password", + "message": "Invalid conversation password" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + } + }, + "summary": "Check validity of a conversation code." + } + }, + "/conversations/join": { + "get": { + "description": " [internal route ID: \"get-conversation-by-reusable-code\"]\n\n", + "operationId": "get-conversation-by-reusable-code", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCoverView" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Get limited conversation information by key/code pair" + }, + "post": { + "description": " [internal route ID: \"join-conversation-by-code-unqualified\"]\n\nIf 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.", + "operationId": "join-conversation-by-code-unqualified", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/JoinConversationByCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation joined" + }, + "204": { + "description": "Conversation unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Join a conversation using a reusable code" + } + }, + "/conversations/list": { + "post": { + "description": " [internal route ID: \"list-conversations\"]\n\n", + "operationId": "list-conversations", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListConversations" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationsResponse" + } + } + }, + "description": "" + } + }, + "summary": "Get conversation metadata for a list of conversation ids" + } + }, + "/conversations/list-ids": { + "post": { + "description": " [internal route ID: \"list-conversation-ids\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "operationId": "list-conversation-ids", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetPaginated_ConversationIds" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationIds_Page" + } + } + }, + "description": "" + } + }, + "summary": "Get all conversation IDs." + } + }, + "/conversations/mls-self": { + "get": { + "description": " [internal route ID: \"get-mls-self-conversation\"]\n\n", + "operationId": "get-mls-self-conversation", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV9" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV9" + } + } + }, + "description": "The MLS self-conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + } + }, + "summary": "Get the user's MLS self-conversation" + } + }, + "/conversations/self": { + "post": { + "description": " [internal route ID: \"create-self-conversation\"]\n\n", + "operationId": "create-self-conversation", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV6" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV6" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + } + }, + "summary": "Create a self-conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}": { + "get": { + "description": " [internal route ID: \"get-conversation\"]\n\n", + "operationId": "get-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get a conversation by ID" + } + }, + "/conversations/{cnv_domain}/{cnv}/access": { + "put": { + "description": " [internal route ID: \"update-conversation-access\"]\n\n", + "operationId": "update-conversation-access", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationAccessData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Access updated" + }, + "204": { + "description": "Access unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid target access (label: `invalid-op`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing modify_conversation_access) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update access modes for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/add-permission": { + "put": { + "description": " [internal route ID: \"update-channel-add-permission\"]\n\n", + "operationId": "update-channel-add-permission", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddPermissionUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Add permissions updated" + }, + "204": { + "description": "Add permissions unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "not-connected", + "operation-denied", + "no-team-member", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid target access (label: `invalid-op`)\n\nUsers are not connected (label: `not-connected`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_add_permissions) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Update the permissions for adding members to a channel" + } + }, + "/conversations/{cnv_domain}/{cnv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-group-info\"]\n\n", + "operationId": "get-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-missing-group-info", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get MLS group information" + } + }, + "/conversations/{cnv_domain}/{cnv}/members": { + "post": { + "description": " [internal route ID: \"add-members-to-conversation\"]\n\n", + "operationId": "add-members-to-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InviteQualified" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation updated" + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-group-id-not-supported", + "message": "The group ID version of the conversation is not supported by one of the federated backends" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-group-id-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Add qualified members to an existing conversation." + }, + "put": { + "description": " [internal route ID: \"replace-members-in-conversation\"]\n\nThis will add any members not already in the conversation, and remove any members not in the provided list except users that are associated via a user group. The given role in the request body will be applied to all added members. The roles of already existing members will not be changed even if these members are included in the request body and their role differs from the role provided in this request.", + "operationId": "replace-members-in-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InviteQualified" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Conversation members replaced" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-group-id-not-supported", + "message": "The group ID version of the conversation is not supported by one of the federated backends" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-group-id-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Replace the members of a conversation." + } + }, + "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { + "delete": { + "description": " [internal route ID: \"remove-member\"]\n\n", + "operationId": "remove-member", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Member removed" + }, + "204": { + "description": "No change" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Remove a member from a conversation" + }, + "put": { + "description": " [internal route ID: \"update-other-member\"]\n\n**Note**: at least one field has to be provided.", + "operationId": "update-other-member", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OtherMemberUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Membership updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update membership of the specified user" + } + }, + "/conversations/{cnv_domain}/{cnv}/message-timer": { + "put": { + "description": " [internal route ID: \"update-conversation-message-timer\"]\n\n", + "operationId": "update-conversation-message-timer", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationMessageTimerUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Message timer updated" + }, + "204": { + "description": "Message timer unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the message timer for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/name": { + "put": { + "description": " [internal route ID: \"update-conversation-name\"]\n\n", + "operationId": "update-conversation-name", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRename" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Name unchanged" + }, + "204": { + "description": "Name updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update conversation name" + } + }, + "/conversations/{cnv_domain}/{cnv}/proteus/messages": { + "post": { + "description": " [internal route ID: \"post-proteus-message\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-proteus-message", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/QualifiedNewOtrMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or Conversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to a conversation (accepts only Protobuf)" + } + }, + "/conversations/{cnv_domain}/{cnv}/protocol": { + "put": { + "description": " [internal route ID: \"update-conversation-protocol\"]\n\n**Note**: Only proteus->mixed upgrade is supported.", + "operationId": "update-conversation-protocol", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProtocolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation updated" + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-migration-criteria-not-satisfied", + "message": "The migration criteria for mixed to MLS protocol transition are not satisfied for this conversation" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-migration-criteria-not-satisfied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe migration criteria for mixed to MLS protocol transition are not satisfied for this conversation (label: `mls-migration-criteria-not-satisfied`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "invalid-op", + "action-denied", + "invalid-protocol-transition" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nProtocol transition is invalid (label: `invalid-protocol-transition`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the protocol of the conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/receipt-mode": { + "put": { + "description": " [internal route ID: \"update-conversation-receipt-mode\"]\n\n", + "operationId": "update-conversation-receipt-mode", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationReceiptModeUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Receipt mode updated" + }, + "204": { + "description": "Receipt mode unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-receipts-not-allowed", + "message": "Read receipts on MLS conversations are not allowed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-receipts-not-allowed", + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Read receipts on MLS conversations are not allowed (label: `mls-receipts-not-allowed`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update receipt mode for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/self": { + "get": { + "description": " [internal route ID: \"get-conversation-self\"]\n\n", + "operationId": "get-conversation-self", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Member" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get self membership properties" + }, + "put": { + "description": " [internal route ID: \"update-conversation-self\"]\n\n**Note**: at least one field has to be provided.", + "operationId": "update-conversation-self", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MemberUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Update successful" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update self membership properties" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}": { + "delete": { + "description": " [internal route ID: \"delete-subconversation\"]\n\n", + "operationId": "delete-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Deletion successful" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Delete an MLS subconversation" + }, + "get": { + "description": " [internal route ID: \"get-subconversation\"]\n\n", + "operationId": "get-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation" + } + } + }, + "description": "Subconversation" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-subconv-unsupported-convtype", + "message": "MLS subconversations are only supported for regular conversations" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-subconv-unsupported-convtype", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS subconversations are only supported for regular conversations (label: `mls-subconv-unsupported-convtype`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get information about an MLS subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-subconversation-group-info\"]\n\n", + "operationId": "get-subconversation-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-missing-group-info", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get MLS group information of subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/self": { + "delete": { + "description": " [internal route ID: \"leave-subconversation\"]\n\n", + "operationId": "leave-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled", + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Leave an MLS subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/typing": { + "post": { + "description": " [internal route ID: \"member-typing-qualified\"]\n\n", + "operationId": "member-typing-qualified", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TypingData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Notification sent" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Sending typing notifications" + } + }, + "/conversations/{cnv}/code": { + "delete": { + "description": " [internal route ID: \"remove-code-unqualified\"]\n\n", + "operationId": "remove-code-unqualified", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation code deleted." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Delete conversation code" + }, + "get": { + "description": " [internal route ID: \"get-code\"]\n\n", + "operationId": "get-code", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + } + }, + "description": "Conversation Code" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Get existing conversation code" + }, + "post": { + "description": " [internal route ID: \"create-conversation-code-unqualified\"]\n\n\nOAuth scope: `write:conversations_code`", + "operationId": "create-conversation-code-unqualified", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateConversationCodeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + } + }, + "description": "Conversation code already exists." + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation code created." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "create-conv-code-conflict", + "message": "Conversation code already exists with a different password setting than the requested one." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "create-conv-code-conflict", + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation code already exists with a different password setting than the requested one. (label: `create-conv-code-conflict`)\n\nThe guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Create or recreate a conversation code" + } + }, + "/conversations/{cnv}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: \"get-conversation-guest-links-status\"]\n\n", + "operationId": "get-conversation-guest-links-status", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." + } + }, + "/conversations/{cnv}/otr/messages": { + "post": { + "description": " [internal route ID: \"post-otr-message-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "operationId": "post-otr-message-unqualified", + "parameters": [ + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + }, + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` or Conversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to a conversation (accepts JSON or Protobuf)" + } + }, + "/conversations/{cnv}/roles": { + "get": { + "description": " [internal route ID: \"get-conversation-roles\"]\n\n", + "operationId": "get-conversation-roles", + "parameters": [ + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRolesList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get existing roles available for the given conversation" + } + }, + "/cookies": { + "get": { + "description": " [internal route ID: \"list-cookies\"]\n\n", + "operationId": "list-cookies", + "parameters": [ + { + "description": "Filter by label (comma-separated list)", + "in": "query", + "name": "labels", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CookieList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CookieList" + } + } + }, + "description": "List of cookies" + } + }, + "summary": "Retrieve the list of cookies currently stored for the user" + } + }, + "/cookies/remove": { + "post": { + "description": " [internal route ID: \"remove-cookies\"]\n\n", + "operationId": "remove-cookies", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveCookies" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Cookies revoked" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Revoke stored cookies" + } + }, + "/custom-backend/by-domain/{domain}": { + "get": { + "description": " [internal route ID: \"get-custom-backend-by-domain\"]\n\n", + "operationId": "get-custom-backend-by-domain", + "parameters": [ + { + "description": "URL-encoded email domain", + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CustomBackend" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "custom-backend-not-found", + "message": "Custom backend not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "custom-backend-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` not found\n\nCustom backend not found (label: `custom-backend-not-found`)" + } + }, + "summary": "Shows information about custom backends related to a given email domain" + } + }, + "/delete": { + "post": { + "description": " [internal route ID: \"verify-delete\"]\n\n", + "operationId": "verify-delete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VerifyDeleteUser" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid verification code (label: `invalid-code`)" + } + }, + "summary": "Verify account deletion with a code." + } + }, + "/domain-verification/{domain}/authorize-team": { + "post": { + "description": " [internal route ID: \"domain-verification-authorize-team\"]\n\n", + "operationId": "domain-verification-authorize-team", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Authorized" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Authorize a team to operate on a verified domain" + } + }, + "/domain-verification/{domain}/backend": { + "post": { + "description": " [internal route ID: \"update-domain-redirect\"]\n\n", + "operationId": "update-domain-redirect", + "parameters": [ + { + "in": "header", + "name": "Authorization", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainRedirectConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Update the domain redirect configuration" + } + }, + "/domain-verification/{domain}/challenges": { + "post": { + "description": " [internal route ID: \"domain-verification-challenge\"]\n\n", + "operationId": "domain-verification-challenge", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainVerificationChallenge" + } + } + }, + "description": "" + } + }, + "summary": "Get a DNS verification challenge" + } + }, + "/domain-verification/{domain}/challenges/{challengeId}": { + "post": { + "description": " [internal route ID: \"verify-challenge\"]\n\n", + "operationId": "verify-challenge", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "challengeId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChallengeToken" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken" + } + } + }, + "description": "" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "domain-verification-failed", + "message": "Domain verification failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-verification-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain verification failed (label: `domain-verification-failed`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "challenge-not-found", + "message": "Challenge not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "challenge-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `challengeId` not found\n\nChallenge not found (label: `challenge-not-found`)" + } + }, + "summary": "Verify a DNS verification challenge" + } + }, + "/domain-verification/{domain}/team": { + "post": { + "description": " [internal route ID: \"update-team-invite\"]\n\n", + "operationId": "update-team-invite", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamInviteConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Update the team-invite configuration" + } + }, + "/domain-verification/{domain}/team/challenges/{challengeId}": { + "post": { + "description": " [internal route ID: \"verify-challenge-team\"]\n\n", + "operationId": "verify-challenge-team", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "challengeId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChallengeToken" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken" + } + } + }, + "description": "" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Verify a DNS verification challenge for a team" + } + }, + "/events": { + "get": { + "description": " [internal route ID: \"consume-events\"]\n\nThis is the rabbitMQ-based variant of \"await-notifications\"", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "consume-events", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Synchronization marker ID", + "in": "query", + "name": "sync_marker", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Consume events over a websocket connection" + } + }, + "/feature-configs": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-user\"]\n\nGets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.If the user is not a member of a team, this will return the personal feature configs (the server defaults).\nOAuth scope: `read:feature_configs`", + "operationId": "get-all-feature-configs-for-user", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AllTeamFeatures" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)" + } + }, + "summary": "Gets feature configs for a user" + } + }, + "/get-domain-registration": { + "post": { + "description": " [internal route ID: \"get-domain-registration\"]\n\n- `due_to_existing_account`: boolean (optional, only present if `domain_redirect` is `no-registration`)\n- `backend`: object (optional, must be present if `domain_redirect` is `backend`)\n - `config_url`: string (required)\n - `webapp_url`: string (optional)\n- `sso_code`: string (optional, must be present if `domain_redirect` is `sso`)", + "operationId": "get-domain-registration", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetDomainRegistrationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainRedirectResponseV10" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-domain", + "message": "Invalid domain" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-domain" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid domain (label: `invalid-domain`)" + } + }, + "summary": "Get domain registration configuration by email" + } + }, + "/handles": { + "post": { + "description": " [internal route ID: \"check-user-handles\"]\n\n", + "operationId": "check-user-handles", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CheckHandles" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + } + }, + "description": "List of free handles" + } + }, + "summary": "Check availability of user handles" + } + }, + "/handles/{handle}": { + "head": { + "description": " [internal route ID: \"check-user-handle\"]\n\n", + "operationId": "check-user-handle", + "parameters": [ + { + "in": "path", + "name": "handle", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Handle is taken" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-handle", + "message": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist)" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-handle" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist) (label: `invalid-handle`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Handle not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`handle` not found\n\nHandle not found (label: `not-found`)" + } + }, + "summary": "Check whether a user handle can be taken" + } + }, + "/identity-providers": { + "get": { + "description": " [internal route ID: \"idp-get-all\"]\n\n", + "operationId": "idp-get-all", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPList" + } + } + }, + "description": "" + } + } + }, + "post": { + "description": " [internal route ID: \"idp-create\"]\n\n", + "operationId": "idp-create", + "parameters": [ + { + "in": "query", + "name": "replaces", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "api_version", + "required": false, + "schema": { + "default": "v2", + "enum": [ + "v1", + "v2" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 32, + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}": { + "delete": { + "description": " [internal route ID: \"idp-delete\"]\n\n", + "operationId": "idp-delete", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "purge", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "204": { + "description": "" + } + } + }, + "get": { + "description": " [internal route ID: \"idp-get\"]\n\n", + "operationId": "idp-get", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + }, + "put": { + "description": " [internal route ID: \"idp-update\"]\n\n", + "operationId": "idp-update", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 32, + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}/raw": { + "get": { + "description": " [internal route ID: \"idp-get-raw\"]\n\n", + "operationId": "idp-get-raw", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/list-connections": { + "post": { + "description": " [internal route ID: \"list-connections\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "operationId": "list-connections", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetPaginated_Connections" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Connections_Page" + } + } + }, + "description": "" + } + }, + "summary": "List the connections to other users, including remote users" + } + }, + "/list-users": { + "post": { + "description": " [internal route ID: \"list-users-by-ids-or-handles\"]\n\nThe 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive.", + "operationId": "list-users-by-ids-or-handles", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListUsersQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListUsersById" + } + } + }, + "description": "" + } + }, + "summary": "List users" + } + }, + "/login": { + "post": { + "description": " [internal route ID: \"login\"]\n\nLogins are throttled at the server's discretion", + "operationId": "login", + "parameters": [ + { + "description": "Request a persistent cookie instead of a session cookie", + "in": "query", + "name": "persist", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Login" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "pending-activation", + "suspended", + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nAccount pending activation (label: `pending-activation`)\n\nAccount suspended (label: `suspended`)\n\nAuthentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Authenticate a user to obtain a cookie and first access token" + } + }, + "/mls/commit-bundles": { + "post": { + "description": " [internal route ID: \"mls-commit-bundle\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.", + "operationId": "mls-commit-bundle", + "requestBody": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/CommitBundle" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + } + }, + "description": "Commit accepted and forwarded" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-invalid-leaf-node-signature", + "message": "Invalid leaf node signature" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-invalid-leaf-node-signature", + "mls-group-id-not-supported", + "mls-welcome-mismatch", + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid leaf node signature (label: `mls-invalid-leaf-node-signature`)\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)\n\nSubmitted group info is inconsistent with the backend group state\n\nThe list of targets of a welcome message does not match the list of new clients in a group (label: `mls-welcome-mismatch`)\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Leaf node signature key does not match the client's key" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch", + "mls-subconv-join-parent-missing", + "missing-legalhold-consent", + "legalhold-not-enabled", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Leaf node signature key does not match the client's key (label: `mls-identity-mismatch`)\n\nMLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "missing_users": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + } + }, + "required": [ + "missing_users" + ], + "type": "object" + } + } + }, + "description": "Group is out of sync\n\nAdding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nA user who is under legal-hold may not participate in MLS conversations (label: `mls-legal-hold-not-allowed`)\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + }, + "422": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" + }, + "properties": { + "code": { + "enum": [ + 422 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-unsupported-proposal", + "mls-unsupported-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Post a MLS CommitBundle" + } + }, + "/mls/key-packages/claim/{user_domain}/{user}": { + "post": { + "description": " [internal route ID: \"mls-key-packages-claim\"]\n\nOnly key packages for the specified ciphersuite are claimed.", + "operationId": "mls-key-packages-claim", + "parameters": [ + { + "in": "path", + "name": "user_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "user", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyPackageBundle" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageBundle" + } + } + }, + "description": "Claimed key packages" + } + }, + "summary": "Claim one key package for each client of the given user" + } + }, + "/mls/key-packages/self/{client}": { + "delete": { + "description": " [internal route ID: \"mls-key-packages-delete\"]\n\n", + "operationId": "mls-key-packages-delete", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteKeyPackages" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "OK" + } + }, + "summary": "Delete all key packages for a given ciphersuite and client" + }, + "post": { + "description": " [internal route ID: \"mls-key-packages-upload\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages.", + "operationId": "mls-key-packages-upload", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageUpload" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Key packages uploaded" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Key package credential does not match qualified client ID" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" + } + }, + "summary": "Upload a fresh batch of key packages" + }, + "put": { + "description": " [internal route ID: \"mls-key-packages-replace\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages. Use this sparingly.", + "operationId": "mls-key-packages-replace", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Comma-separated list of ciphersuites in hex format (e.g. 0xf031)", + "in": "query", + "name": "ciphersuites", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageUpload" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Key packages replaced" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `ciphersuites`\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Key package credential does not match qualified client ID" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" + } + }, + "summary": "Upload a fresh batch of key packages and replace the old ones" + } + }, + "/mls/key-packages/self/{client}/count": { + "get": { + "description": " [internal route ID: \"mls-key-packages-count\"]\n\n", + "operationId": "mls-key-packages-count", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnKeyPackages" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnKeyPackages" + } + } + }, + "description": "Number of key packages" + } + }, + "summary": "Return the number of unclaimed key packages for a given ciphersuite and client" + } + }, + "/mls/messages": { + "post": { + "description": " [internal route ID: \"mls-message\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.", + "operationId": "mls-message", + "requestBody": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/MLSMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-invalid-leaf-node-signature", + "message": "Invalid leaf node signature" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-invalid-leaf-node-signature", + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid leaf node signature (label: `mls-invalid-leaf-node-signature`)\n\nSubmitted group info is inconsistent with the backend group state\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-subconv-join-parent-missing", + "message": "MLS client cannot join the subconversation because it is not member of the parent conversation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-subconv-join-parent-missing", + "missing-legalhold-consent", + "legalhold-not-enabled", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "missing_users": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + } + }, + "required": [ + "missing_users" + ], + "type": "object" + } + } + }, + "description": "Group is out of sync\n\nAdding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + }, + "422": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" + }, + "properties": { + "code": { + "enum": [ + 422 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-unsupported-proposal", + "mls-unsupported-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Post an MLS message" + } + }, + "/mls/public-keys": { + "get": { + "description": " [internal route ID: \"mls-public-keys\"]\n\nThe format of the returned key is determined by the `format` query parameter:\n - raw (default): base64-encoded raw public keys\n - jwk: keys are nested objects in JWK format.", + "operationId": "mls-public-keys", + "parameters": [ + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "enum": [ + "raw", + "jwk" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + } + }, + "description": "Public keys" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + } + }, + "summary": "Get public keys used by the backend to sign external proposals" + } + }, + "/mls/reset-conversation": { + "post": { + "description": " [internal route ID: \"mls-reset-conversation\"]\n\n", + "operationId": "mls-reset-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Conversation reset" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error", + "mls-group-id-not-supported", + "mls-federated-reset-not-supported", + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS protocol error (label: `mls-protocol-error`)\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)\n\nReset is not supported by the owning backend of the conversation (label: `mls-federated-reset-not-supported`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`) or `body`" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "action-denied", + "message": "Insufficient authorization (missing leave_conversation)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "action-denied", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Reset an MLS conversation to epoch 0" + } + }, + "/notifications": { + "get": { + "description": " [internal route ID: \"get-notifications\"]\n\n", + "operationId": "get-notifications", + "parameters": [ + { + "description": "Only return notifications more recent than this", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Maximum number of notifications to return", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 100, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList" + } + } + }, + "description": "Notification list" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch notifications" + } + }, + "/notifications/last": { + "get": { + "description": " [internal route ID: \"get-last-notification\"]\n\n", + "operationId": "get-last-notification", + "parameters": [ + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + } + }, + "description": "Notification found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch the last notification" + } + }, + "/notifications/{id}": { + "get": { + "description": " [internal route ID: \"get-notification-by-id\"]\n\n", + "operationId": "get-notification-by-id", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + } + }, + "description": "Notification found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`id` or Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch a notification by ID" + } + }, + "/oauth/applications": { + "get": { + "description": " [internal route ID: \"get-oauth-applications\"]\n\nGet all OAuth applications with active account access for a user.", + "operationId": "get-oauth-applications", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication" + }, + "type": "array" + } + } + }, + "description": "OAuth applications found" + } + }, + "summary": "Get OAuth applications with account access" + } + }, + "/oauth/applications/{OAuthClientId}/sessions": { + "delete": { + "description": " [internal route ID: \"revoke-oauth-account-access\"]\n\n", + "operationId": "revoke-oauth-account-access", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReqBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "OAuth application access revoked" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Revoke account access from an OAuth application" + } + }, + "/oauth/applications/{OAuthClientId}/sessions/{RefreshTokenId}": { + "delete": { + "description": " [internal route ID: \"delete-oauth-refresh-token\"]\n\nRevoke an active OAuth session by providing the refresh token ID.", + "operationId": "delete-oauth-refresh-token", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "The ID of the refresh token", + "in": "path", + "name": "RefreshTokenId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReqBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`OAuthClientId` or `RefreshTokenId` not found\n\nOAuth client not found (label: `not-found`)" + } + }, + "summary": "Revoke an active OAuth session" + } + }, + "/oauth/authorization/codes": { + "post": { + "description": " [internal route ID: \"create-oauth-auth-code\"]\n\nCurrently only supports the 'code' response type, which corresponds to the authorization code flow.", + "operationId": "create-oauth-auth-code", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthAuthorizationCodeRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "redirect-url-miss-match" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "redirect-url-miss-match" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Bad Request\n\nThe redirect URL does not match the one registered with the client (label: `redirect-url-miss-match`) or `body`", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Create an OAuth authorization code" + } + }, + "/oauth/clients/{OAuthClientId}": { + "get": { + "description": " [internal route ID: \"get-oauth-client\"]\n\n", + "operationId": "get-oauth-client", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClient" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthClient" + } + } + }, + "description": "OAuth client found" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "OAuth is disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth is disabled (label: `forbidden`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`OAuthClientId` or OAuth client not found (label: `not-found`)\n\nOAuth client not found (label: `not-found`)" + } + }, + "summary": "Get OAuth client information" + } + }, + "/oauth/revoke": { + "post": { + "description": " [internal route ID: \"revoke-oauth-refresh-token\"]\n\nRevoke an access token.", + "operationId": "revoke-oauth-refresh-token", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthRevokeRefreshTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "Invalid refresh token" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid refresh token (label: `forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth client not found (label: `not-found`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "summary": "Revoke an OAuth refresh token" + } + }, + "/oauth/token": { + "post": { + "description": " [internal route ID: \"create-oauth-access-token\"]\n\nObtain a new access token from an authorization code or a refresh token.", + "operationId": "create-oauth-access-token", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid_grant", + "message": "Invalid grant" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid_grant", + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid grant (label: `invalid_grant`)\n\nInvalid client credentials (label: `forbidden`)\n\nInvalid grant type (label: `forbidden`)\n\nInvalid refresh token (label: `forbidden`)\n\nOAuth is disabled (label: `forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth client not found (label: `not-found`)\n\nOAuth authorization code not found (label: `not-found`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "summary": "Create an OAuth access token" + } + }, + "/one2one-conversations": { + "post": { + "description": " [internal route ID: \"create-one-to-one-conversation\"]\n\n", + "operationId": "create-one-to-one-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewOne2OneConv" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV3" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV3" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "not-connected", + "no-team-member", + "non-binding-team-members", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nBoth users must be members of the same binding team (label: `non-binding-team-members`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "non-binding-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)\n\nNot a member of a binding team (label: `non-binding-team`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a 1:1 conversation" + } + }, + "/one2one-conversations/{usr_domain}/{usr}": { + "get": { + "description": " [internal route ID: \"get-one-to-one-mls-conversation\"]\n\n", + "operationId": "get-one-to-one-mls-conversation", + "parameters": [ + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "enum": [ + "raw", + "jwk" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSOne2OneConversation_SomeKey" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSOne2OneConversation_SomeKey" + } + } + }, + "description": "MLS 1-1 conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "not-connected", + "message": "Users are not connected" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-connected" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Users are not connected (label: `not-connected`)" + } + }, + "summary": "Get an MLS 1:1 conversation" + } + }, + "/password-reset": { + "post": { + "description": " [internal route ID: \"post-password-reset\"]\n\n", + "operationId": "post-password-reset", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewPasswordReset" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Password reset code created and sent by email." + } + }, + "summary": "Initiate a password reset." + } + }, + "/password-reset/complete": { + "post": { + "description": " [internal route ID: \"post-password-reset-complete\"]\n\n", + "operationId": "post-password-reset-complete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CompletePasswordReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/properties": { + "delete": { + "description": " [internal route ID: \"clear-properties\"]\n\n", + "operationId": "clear-properties", + "responses": { + "200": { + "description": "Properties cleared" + } + }, + "summary": "Clear all properties" + }, + "get": { + "description": " [internal route ID: \"list-property-keys\"]\n\n", + "operationId": "list-property-keys", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + } + }, + "description": "List of property keys" + } + }, + "summary": "List all property keys" + } + }, + "/properties-values": { + "get": { + "description": " [internal route ID: \"list-properties\"]\n\n", + "operationId": "list-properties", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyKeysAndValues" + } + } + }, + "description": "" + } + }, + "summary": "List all properties with key and value" + } + }, + "/properties/{key}": { + "delete": { + "description": " [internal route ID: \"delete-property\"]\n\n", + "operationId": "delete-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Property deleted" + } + }, + "summary": "Delete a property" + }, + "get": { + "description": " [internal route ID: \"get-property\"]\n\n", + "operationId": "get-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + } + }, + "description": "The property value" + }, + "404": { + "description": "`key` or Property not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a property value" + }, + "put": { + "description": " [internal route ID: \"set-property\"]\n\n", + "operationId": "set-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Property set" + } + }, + "summary": "Set a user property" + } + }, + "/provider": { + "delete": { + "description": " [internal route ID: \"provider-delete\"]\n\n", + "operationId": "provider-delete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteProvider" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Delete a provider" + }, + "get": { + "description": " [internal route ID: \"provider-get-account\"]\n\n", + "operationId": "provider-get-account", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Provider not found. (label: `not-found`)\n\nProvider not found. (label: `not-found`)" + } + }, + "summary": "Get account" + }, + "put": { + "description": " [internal route ID: \"provider-update\"]\n\n", + "operationId": "provider-update", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateProvider" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-provider", + "message": "The provider does not exist." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Update a provider" + } + }, + "/provider/activate": { + "get": { + "description": " [internal route ID: \"provider-activate\"]\n\n", + "operationId": "provider-activate", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderActivationResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProviderActivationResponse" + } + } + }, + "description": "" + }, + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Activate a provider" + } + }, + "/provider/assets": { + "post": { + "description": " [internal route ID: (\"assets-upload-v3\", provider)]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload-v3_provider", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/provider/assets/{key}": { + "delete": { + "description": " [internal route ID: (\"assets-delete-v3\", provider)]\n\n", + "operationId": "assets-delete-v3_provider", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: (\"assets-download-v3\", provider)]\n\n", + "operationId": "assets-download-v3_provider", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/provider/email": { + "put": { + "description": " [internal route ID: \"provider-update-email\"]\n\n", + "operationId": "provider-update-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-provider", + "message": "The provider does not exist." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Update a provider email" + } + }, + "/provider/login": { + "post": { + "description": " [internal route ID: \"provider-login\"]\n\n", + "operationId": "provider-login", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProviderLogin" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Login as a provider" + } + }, + "/provider/password": { + "put": { + "description": " [internal route ID: \"provider-update-password\"]\n\n", + "operationId": "provider-update-password", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Update a provider password" + } + }, + "/provider/password-reset": { + "post": { + "description": " [internal route ID: \"provider-password-reset\"]\n\n", + "operationId": "provider-password-reset", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReset" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ", + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)\n\nA password reset is already in progress. (label: `code-exists`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Begin a password reset" + } + }, + "/provider/password-reset/complete": { + "post": { + "description": " [internal route ID: \"provider-password-reset-complete\"]\n\n", + "operationId": "provider-password-reset-complete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CompletePasswordReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "invalid-code", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Complete a password reset" + } + }, + "/provider/register": { + "post": { + "description": " [internal route ID: \"provider-register\"]\n\n", + "operationId": "provider-register", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewProvider" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `X-Forwarded-For`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Register a new provider" + } + }, + "/provider/services": { + "get": { + "description": " [internal route ID: \"get-provider-services\"]\n\n", + "operationId": "get-provider-services", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Service" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List provider services" + }, + "post": { + "description": " [internal route ID: \"post-provider-services\"]\n\n", + "operationId": "post-provider-services", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewService" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewServiceResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewServiceResponse" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Create a new service" + } + }, + "/provider/services/{service-id}": { + "delete": { + "description": " [internal route ID: \"delete-provider-services-by-service-id\"]\n\n", + "operationId": "delete-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteService" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Delete service" + }, + "get": { + "description": " [internal route ID: \"get-provider-services-by-service-id\"]\n\n", + "operationId": "get-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Service" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Get provider service by service id" + }, + "put": { + "description": " [internal route ID: \"put-provider-services-by-service-id\"]\n\n", + "operationId": "put-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateService" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Provider service updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nProvider not found. (label: `not-found`)\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Update provider service" + } + }, + "/provider/services/{service-id}/connection": { + "put": { + "description": " [internal route ID: \"put-provider-services-connection-by-service-id\"]\n\n", + "operationId": "put-provider-services-connection-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceConn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Provider service connection updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Update provider service connection" + } + }, + "/providers/{pid}": { + "get": { + "description": " [internal route ID: \"provider-get-profile\"]\n\n", + "operationId": "provider-get-profile", + "parameters": [ + { + "in": "path", + "name": "pid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`pid` or Provider not found. (label: `not-found`)" + } + }, + "summary": "Get profile" + } + }, + "/providers/{provider-id}/services": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id\"]\n\n", + "operationId": "get-provider-services-by-provider-id", + "parameters": [ + { + "in": "path", + "name": "provider-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ServiceProfile" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get provider services by provider id" + } + }, + "/providers/{provider-id}/services/{service-id}": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id-and-service-id\"]\n\n", + "operationId": "get-provider-services-by-provider-id-and-service-id", + "parameters": [ + { + "in": "path", + "name": "provider-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfile" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`provider-id` or `service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Get provider service by provider id and service id" + } + }, + "/proxy/giphy/v1/gifs": {}, + "/proxy/googlemaps/api/staticmap": {}, + "/proxy/googlemaps/maps/api/geocode": {}, + "/proxy/soundcloud/resolve": {}, + "/proxy/soundcloud/stream": {}, + "/proxy/spotify/api/token": {}, + "/proxy/youtube/v3": {}, + "/push/tokens": { + "get": { + "description": " [internal route ID: \"get-push-tokens\"]\n\n", + "operationId": "get-push-tokens", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushTokenList" + } + } + }, + "description": "" + } + }, + "summary": "List the user's registered push tokens" + }, + "post": { + "description": " [internal route ID: \"register-push-token\"]\n\n", + "operationId": "register-push-token", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + } + }, + "description": "Push token registered", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "apns-voip-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "apns-voip-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Adding APNS_VOIP tokens is not supported (label: `apns-voip-not-supported`) or `body`" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "app-not-found", + "message": "App does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "app-not-found", + "invalid-token" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "app-not-found", + "message": "App does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "app-not-found", + "invalid-token" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "App does not exist (label: `app-not-found`)\n\nInvalid push token (label: `invalid-token`)" + }, + "413": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many concurrent calls to SNS; is SNS down? (label: `sns-thread-budget-reached`)\n\nPush token length must be < 8192 for GCM or 400 for APNS (label: `token-too-long`)\n\nTried to add token to endpoint resulting in metadata length > 2048 (label: `metadata-too-long`)" + } + }, + "summary": "Register a native push token" + } + }, + "/push/tokens/{pid}": { + "delete": { + "description": " [internal route ID: \"delete-push-token\"]\n\n", + "operationId": "delete-push-token", + "parameters": [ + { + "description": "The push token to delete", + "in": "path", + "name": "pid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Push token unregistered" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Push token not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Push token not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`pid` or Push token not found (label: `not-found`)" + } + }, + "summary": "Unregister a native push token" + } + }, + "/register": { + "post": { + "description": " [internal route ID: \"register\"]\n\nIf the environment where the registration takes place is private and a registered email address is not whitelisted, a 403 error is returned.", + "operationId": "register", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewUser" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "description": "User created and pending activation", + "headers": { + "Location": { + "description": "UserId", + "schema": { + "format": "uuid", + "type": "string" + } + }, + "Set-Cookie": { + "description": "Cookie", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email", + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email", + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)\n\nInvalid mobile phone number (label: `invalid-phone`) or `body` or `X-Forwarded-For`" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted", + "ephemeral-user-creation-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted", + "ephemeral-user-creation-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorized e-mail address (label: `unauthorized`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nToo many members in this team. (label: `too-many-team-members`)\n\nThis instance does not allow creation of personal users or teams. (label: `user-creation-restricted`)\n\nEphemeral user creation is disabled on this instance. (label: `ephemeral-user-creation-disabled`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "User does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "User does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "User does not exist (label: `invalid-code`)\n\nInvalid activation code (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Register a new user." + } + }, + "/scim/auth-tokens": { + "delete": { + "description": " [internal route ID: \"auth-tokens-delete\"]\n\n", + "operationId": "auth-tokens-delete", + "parameters": [ + { + "in": "query", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + }, + "get": { + "description": " [internal route ID: \"auth-tokens-list\"]\n\n", + "operationId": "auth-tokens-list", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ScimTokenList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + }, + "post": { + "description": " [internal route ID: \"auth-tokens-create\"]\n\n", + "operationId": "auth-tokens-create", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateScimToken" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateScimTokenResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + } + }, + "/scim/auth-tokens/{id}": { + "put": { + "description": " [internal route ID: \"auth-tokens-put-name\"]\n\n", + "operationId": "auth-tokens-put-name", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ScimTokenName" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + } + }, + "/search/contacts": { + "get": { + "description": " [internal route ID: \"search-contacts\"]\n\n", + "operationId": "search-contacts", + "parameters": [ + { + "description": "Search query", + "in": "query", + "name": "q", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", + "in": "query", + "name": "domain", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult_Contact" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Search for users" + } + }, + "/self": { + "delete": { + "description": " [internal route ID: \"delete-self\"]\n\nif the account has a verified identity, a verification code is sent and needs to be confirmed to authorise the deletion. if the account has no verified identity but a password, it must be provided. if password is correct, or if neither a verified identity nor a password exists, account deletion is scheduled immediately.", + "operationId": "delete-self", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteUser" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout" + } + } + }, + "description": "Deletion is pending verification with a code." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-self-delete-for-team-owner", + "message": "Team owners are not allowed to delete themselves; ask a fellow owner" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-self-delete-for-team-owner", + "pending-delete", + "missing-auth", + "invalid-credentials", + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team owners are not allowed to delete themselves; ask a fellow owner (label: `no-self-delete-for-team-owner`)\n\nA verification code for account deletion is still pending (label: `pending-delete`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)" + } + }, + "summary": "Initiate account deletion." + }, + "get": { + "description": " [internal route ID: \"get-self\"]\n\n\nOAuth scope: `read:self`", + "operationId": "get-self", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "description": "" + } + }, + "summary": "Get your own profile" + }, + "put": { + "description": " [internal route ID: \"put-self\"]\n\n", + "operationId": "put-self", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User updated" + } + }, + "summary": "Update your profile." + } + }, + "/self/email": { + "delete": { + "description": " [internal route ID: \"remove-email\"]\n\nYour email address can only be removed if you also have a phone number.", + "operationId": "remove-email", + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The last user identity cannot be removed. (label: `last-identity`)\n\nThe user has no verified email (label: `no-identity`)" + } + }, + "summary": "Remove your email address." + } + }, + "/self/handle": { + "put": { + "description": " [internal route ID: \"change-handle\"]\n\n", + "operationId": "change-handle", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/HandleUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Handle Changed" + } + }, + "summary": "Change your handle." + } + }, + "/self/locale": { + "put": { + "description": " [internal route ID: \"change-locale\"]\n\n", + "operationId": "change-locale", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LocaleUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Local Changed" + } + }, + "summary": "Change your locale." + } + }, + "/self/password": { + "head": { + "description": " [internal route ID: \"check-password-exists\"]\n\n", + "operationId": "check-password-exists", + "responses": { + "200": { + "description": "Password is set" + }, + "404": { + "description": "Password is not set" + } + }, + "summary": "Check that your password is set." + }, + "put": { + "description": " [internal route ID: \"change-password\"]\n\n", + "operationId": "change-password", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password Changed" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe user has no verified email (label: `no-identity`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password change, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Change your password." + } + }, + "/self/supported-protocols": { + "put": { + "description": " [internal route ID: \"change-supported-protocols\"]\n\n", + "operationId": "change-supported-protocols", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SupportedProtocolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Supported protocols changed" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-protocol-error", + "message": "MLS protocol cannot be removed" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS protocol cannot be removed (label: `mls-protocol-error`)" + } + }, + "summary": "Change your supported protocols" + } + }, + "/services": { + "get": { + "description": " [internal route ID: \"get-services\"]\n\n", + "operationId": "get-services", + "parameters": [ + { + "in": "query", + "name": "tags", + "required": false, + "schema": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "start", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfilePage" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List services" + } + }, + "/services/tags": { + "get": { + "description": " [internal route ID: \"get-services-tags\"]\n\n", + "operationId": "get-services-tags", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceTagList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get services tags" + } + }, + "/sso/finalize-login": { + "post": { + "deprecated": true, + "description": " [internal route ID: \"auth-resp-legacy\"]\n\nDEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "operationId": "auth-resp-legacy", + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/finalize-login/{team}": { + "post": { + "description": " [internal route ID: \"auth-resp\"]\n\n", + "operationId": "auth-resp", + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/initiate-login/{idp}": { + "get": { + "description": " [internal route ID: \"auth-req\"]\n\n", + "operationId": "auth-req", + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/FormRedirect" + } + } + }, + "description": "" + } + } + }, + "head": { + "description": " [internal route ID: \"auth-req-precheck\"]\n\n", + "operationId": "auth-req-precheck", + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": {} + }, + "description": "" + } + } + } + }, + "/sso/metadata": { + "get": { + "deprecated": true, + "description": " [internal route ID: \"sso-metadata\"]\n\nDEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "operationId": "sso-metadata", + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/metadata/{team}": { + "get": { + "description": " [internal route ID: \"sso-team-metadata\"]\n\n", + "operationId": "sso-team-metadata", + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/settings": { + "get": { + "description": " [internal route ID: \"sso-settings\"]\n\n", + "operationId": "sso-settings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SsoSettings" + } + } + }, + "description": "" + } + } + } + }, + "/system/settings": { + "get": { + "description": " [internal route ID: \"get-system-settings\"]\n\n", + "operationId": "get-system-settings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettings" + } + } + }, + "description": "" + } + }, + "summary": "Returns a curated set of system configuration settings for authorized users." + } + }, + "/system/settings/unauthorized": { + "get": { + "description": " [internal route ID: \"get-system-settings-unauthorized\"]\n\n", + "operationId": "get-system-settings-unauthorized", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettingsPublic" + } + } + }, + "description": "" + } + }, + "summary": "Returns a curated set of system configuration settings." + } + }, + "/teams/invitations/accept": { + "post": { + "description": " [internal route ID: \"accept-team-invitation\"]\n\n", + "operationId": "accept-team-invitation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AcceptTeamInvitation" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Team invitation accepted." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-auth", + "message": "Re-authentication via password required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-auth", + "invalid-credentials", + "missing-identity", + "too-many-team-members" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Re-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\n\nToo many members in this team. (label: `too-many-team-members`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)\n\nNo pending invitations exists. (label: `not-found`)" + } + }, + "summary": "Accept a team invitation, changing a personal account into a team member account." + } + }, + "/teams/invitations/by-email": { + "head": { + "description": " [internal route ID: \"head-team-invitations\"]\n\n", + "operationId": "head-team-invitations", + "parameters": [ + { + "description": "Email address", + "in": "query", + "name": "email", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Pending invitation exists." + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "No pending invitations exists." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "No pending invitations exists." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "No pending invitations exists. (label: `not-found`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Multiple conflicting invitations to different teams exists. (label: `conflicting-invitations`)" + } + }, + "summary": "Check if there is an invitation pending given an email address." + } + }, + "/teams/invitations/info": { + "get": { + "description": " [internal route ID: \"get-team-invitation-info\"]\n\n", + "operationId": "get-team-invitation-info", + "parameters": [ + { + "description": "Invitation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationUserView" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationUserView" + } + } + }, + "description": "Invitation info" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `code`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get invitation info given a code." + } + }, + "/teams/notifications": { + "get": { + "description": " [internal route ID: \"get-team-notifications\"]\n\nThis is a work-around for scalability issues with gundeck user event fan-out. It does not track all team-wide events, but only `member-join`.\nNote that `/teams/notifications` behaves differently from `/notifications`:\n- If there is a gap between the notification id requested with `since` and the available data, team queues respond with 200 and the data that could be found. They do NOT respond with status 404, but valid data in the body.\n- The notification with the id given via `since` is included in the response if it exists. You should remove this and only use it to decide whether there was a gap between your last request and this one.\n- If the notification id does *not* exist, you get the more recent events from the queue (instead of all of them). This can be done because a notification id is a UUIDv1, which is essentially a time stamp.\n- There is no corresponding `/last` end-point to get only the most recent event. That end-point was only useful to avoid having to pull the entire queue. In team queues, if you have never requested the queue before and have no prior notification id, just pull with timestamp 'now'.", + "operationId": "get-team-notifications", + "parameters": [ + { + "description": "Notification id to start with in the response (UUIDv1)", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum number of events to return (1..10000; default: 1000)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-notification-id", + "message": "Could not parse notification id (must be UUIDv1)." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-notification-id" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `size` or `since`\n\nCould not parse notification id (must be UUIDv1). (label: `invalid-notification-id`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)" + } + }, + "summary": "Read recently added team members from team queue" + } + }, + "/teams/{team-id}/services/whitelist": { + "post": { + "description": " [internal route ID: \"post-team-whitelist-by-team-id\"]\n\n", + "operationId": "post-team-whitelist-by-team-id", + "parameters": [ + { + "in": "path", + "name": "team-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceWhitelist" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "UpdateServiceWhitelistRespChanged" + }, + "204": { + "description": "UpdateServiceWhitelistRespUnchanged" + } + }, + "summary": "Update service whitelist" + } + }, + "/teams/{team-id}/services/whitelisted": { + "get": { + "description": " [internal route ID: \"get-whitelisted-services-by-team-id\"]\n\n", + "operationId": "get-whitelisted-services-by-team-id", + "parameters": [ + { + "in": "path", + "name": "team-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "prefix", + "required": false, + "schema": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + { + "in": "query", + "name": "filter_disabled", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfilePage" + } + } + }, + "description": "" + } + }, + "summary": "Get whitelisted services by team id" + } + }, + "/teams/{teamId}/registered-domains": { + "get": { + "description": " [internal route ID: \"get-all-registered-domains\"]\n\n", + "operationId": "get-all-registered-domains", + "parameters": [ + { + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RegisteredDomains" + } + } + }, + "description": "" + } + }, + "summary": "Get all registered domains" + } + }, + "/teams/{teamId}/registered-domains/{domain}": { + "delete": { + "description": " [internal route ID: \"delete-registered-domain\"]\n\n", + "operationId": "delete-registered-domain", + "parameters": [ + { + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Delete a registered domain" + } + }, + "/teams/{tid}": { + "delete": { + "description": " [internal route ID: \"delete-team\"]\n\n", + "operationId": "delete-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamDeleteData" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Team is scheduled for removal" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Verification code required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Verification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (missing DeleteTeam) (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "503": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 503, + "label": "queue-full", + "message": "The delete queue is full; no further delete requests can be processed at the moment" + }, + "properties": { + "code": { + "enum": [ + 503 + ], + "type": "integer" + }, + "label": { + "enum": [ + "queue-full" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The delete queue is full; no further delete requests can be processed at the moment (label: `queue-full`)" + } + }, + "summary": "Delete a team" + }, + "get": { + "description": " [internal route ID: \"get-team\"]\n\n", + "operationId": "get-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Team" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get a team by ID" + }, + "put": { + "description": " [internal route ID: \"update-team\"]\n\n", + "operationId": "update-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamUpdateData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Team updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions (missing SetTeamData)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (missing SetTeamData) (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Update team properties" + } + }, + "/teams/{tid}/apps": { + "post": { + "description": " [internal route ID: \"create-app\"]\n\n", + "operationId": "create-app", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewApp" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreatedApp" + } + } + }, + "description": "" + } + }, + "summary": "Create a new app" + } + }, + "/teams/{tid}/apps/{app}/cookies": { + "post": { + "description": " [internal route ID: \"refresh-app-cookie\"]\n\n", + "operationId": "refresh-app-cookie", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "app", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RefreshAppCookieResponse" + } + } + }, + "description": "" + } + }, + "summary": "Get a new app authentication token" + } + }, + "/teams/{tid}/apps/{uid}": { + "get": { + "description": " [internal route ID: \"get-app\"]\n\n", + "operationId": "get-app", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetApp" + } + } + }, + "description": "" + } + }, + "summary": "Get app" + } + }, + "/teams/{tid}/channels/search": { + "get": { + "description": " [internal route ID: \"search-channels\"]\n\n", + "operationId": "search-channels", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Search string", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "sort_order", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "page_size", + "required": false, + "schema": { + "description": "integer from [1..500]", + "type": "number" + } + }, + { + "description": "`name` of the last seen channel of the current page, used to get the next page.", + "in": "query", + "name": "last_seen_name", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "`id` of the last seen channel, used to get the next page, used as a tie breaker. **Must** be sent to get the next page.", + "in": "query", + "name": "last_seen_id", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "discoverable", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationPage" + } + } + }, + "description": "" + } + }, + "summary": "Search channels" + } + }, + "/teams/{tid}/collaborators": { + "get": { + "description": " [internal route ID: \"get-team-collaborators\"]\n\n", + "operationId": "get-team-collaborators", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TeamCollaborator" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/TeamCollaborator" + }, + "type": "array" + } + } + }, + "description": "Return collaborators" + } + }, + "summary": "Get all collaborators of the team." + }, + "post": { + "description": " [internal route ID: \"add-team-collaborator\"]\n\n", + "operationId": "add-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewTeamCollaborator" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "summary": "Add a collaborator to the team." + } + }, + "/teams/{tid}/collaborators/{uid}": { + "delete": { + "description": " [internal route ID: \"remove-team-collaborator\"]\n\n", + "operationId": "remove-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + } + }, + "summary": "Remove a collaborator from the team." + }, + "put": { + "description": " [internal route ID: \"update-team-collaborator\"]\n\n", + "operationId": "update-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission" + }, + "type": "array", + "uniqueItems": true + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "summary": "Update a collaborator permissions from the team." + } + }, + "/teams/{tid}/conversations": { + "get": { + "description": " [internal route ID: \"get-team-conversations\"]\n\n", + "operationId": "get-team-conversations", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamConversationList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + } + }, + "summary": "Get team conversations" + } + }, + "/teams/{tid}/conversations/roles": { + "get": { + "description": " [internal route ID: \"get-team-conversation-roles\"]\n\n", + "operationId": "get-team-conversation-roles", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRolesList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get existing roles available for the given team" + } + }, + "/teams/{tid}/conversations/{cid}": { + "delete": { + "description": " [internal route ID: \"delete-team-conversation\"]\n\n", + "operationId": "delete-team-conversation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Conversation deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing delete_conversation) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Remove a team conversation" + }, + "get": { + "description": " [internal route ID: \"get-team-conversation\"]\n\n", + "operationId": "get-team-conversation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamConversation" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get one team conversation" + } + }, + "/teams/{tid}/features": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-team\"]\n\nGets feature configs for a team. User must be a member of the team and have permission to view team features.", + "operationId": "get-all-feature-configs-for-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AllTeamFeatures" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Gets feature configs for a team" + } + }, + "/teams/{tid}/features/allowedGlobalOperations": { + "get": { + "description": " [internal route ID: (\"get\", AllowedGlobalOperationsConfig)]\n\n", + "operationId": "get_AllowedGlobalOperationsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AllowedGlobalOperationsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for allowedGlobalOperations" + } + }, + "/teams/{tid}/features/appLock": { + "get": { + "description": " [internal route ID: (\"get\", AppLockConfigB)]\n\n", + "operationId": "get_AppLockConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for appLock" + }, + "put": { + "description": " [internal route ID: (\"put\", AppLockConfigB)]\n\n", + "operationId": "put_AppLockConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppLockConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for appLock" + } + }, + "/teams/{tid}/features/apps": { + "get": { + "description": " [internal route ID: (\"get\", AppsConfig)]\n\n", + "operationId": "get_AppsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for apps" + } + }, + "/teams/{tid}/features/assetAuditLog": { + "get": { + "description": " [internal route ID: (\"get\", AssetAuditLogConfig)]\n\n", + "operationId": "get_AssetAuditLogConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AssetAuditLogConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for assetAuditLog" + } + }, + "/teams/{tid}/features/cells": { + "get": { + "description": " [internal route ID: (\"get\", CellsConfigB)]\n\n", + "operationId": "get_CellsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CellsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for cells" + }, + "put": { + "description": " [internal route ID: (\"put\", CellsConfigB)]\n\n", + "operationId": "put_CellsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CellsConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CellsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for cells" + } + }, + "/teams/{tid}/features/cellsInternal": { + "get": { + "description": " [internal route ID: (\"get\", CellsInternalConfigB)]\n\n", + "operationId": "get_CellsInternalConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CellsInternalConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for cellsInternal" + } + }, + "/teams/{tid}/features/channels": { + "get": { + "description": " [internal route ID: (\"get\", ChannelsConfigB)]\n\n", + "operationId": "get_ChannelsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChannelsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for channels" + }, + "put": { + "description": " [internal route ID: (\"put\", ChannelsConfigB)]\n\n", + "operationId": "put_ChannelsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChannelsConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChannelsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for channels" + } + }, + "/teams/{tid}/features/chatBubbles": { + "get": { + "description": " [internal route ID: (\"get\", ChatBubblesConfig)]\n\n", + "operationId": "get_ChatBubblesConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChatBubblesConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for chatBubbles" + } + }, + "/teams/{tid}/features/classifiedDomains": { + "get": { + "description": " [internal route ID: (\"get\", ClassifiedDomainsConfig)]\n\n", + "operationId": "get_ClassifiedDomainsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for classifiedDomains" + } + }, + "/teams/{tid}/features/conferenceCalling": { + "get": { + "description": " [internal route ID: (\"get\", ConferenceCallingConfigB)]\n\n", + "operationId": "get_ConferenceCallingConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for conferenceCalling" + }, + "put": { + "description": " [internal route ID: (\"put\", ConferenceCallingConfigB)]\n\n", + "operationId": "put_ConferenceCallingConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConferenceCallingConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for conferenceCalling" + } + }, + "/teams/{tid}/features/consumableNotifications": { + "get": { + "description": " [internal route ID: (\"get\", ConsumableNotificationsConfig)]\n\n", + "operationId": "get_ConsumableNotificationsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConsumableNotificationsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for consumableNotifications" + } + }, + "/teams/{tid}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: (\"get\", GuestLinksConfig)]\n\n", + "operationId": "get_GuestLinksConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for conversationGuestLinks" + }, + "put": { + "description": " [internal route ID: (\"put\", GuestLinksConfig)]\n\n", + "operationId": "put_GuestLinksConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for conversationGuestLinks" + } + }, + "/teams/{tid}/features/digitalSignatures": { + "get": { + "description": " [internal route ID: (\"get\", DigitalSignaturesConfig)]\n\n", + "operationId": "get_DigitalSignaturesConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DigitalSignaturesConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for digitalSignatures" + } + }, + "/teams/{tid}/features/domainRegistration": { + "get": { + "description": " [internal route ID: (\"get\", DomainRegistrationConfig)]\n\n", + "operationId": "get_DomainRegistrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainRegistrationConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for domainRegistration" + } + }, + "/teams/{tid}/features/enforceFileDownloadLocation": { + "get": { + "description": " [internal route ID: (\"get\", EnforceFileDownloadLocationConfigB)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "operationId": "get_EnforceFileDownloadLocationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for enforceFileDownloadLocation" + }, + "put": { + "description": " [internal route ID: (\"put\", EnforceFileDownloadLocationConfigB)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "operationId": "put_EnforceFileDownloadLocationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for enforceFileDownloadLocation" + } + }, + "/teams/{tid}/features/exposeInvitationURLsToTeamAdmin": { + "get": { + "description": " [internal route ID: (\"get\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "operationId": "get_ExposeInvitationURLsToTeamAdminConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for exposeInvitationURLsToTeamAdmin" + }, + "put": { + "description": " [internal route ID: (\"put\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "operationId": "put_ExposeInvitationURLsToTeamAdminConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for exposeInvitationURLsToTeamAdmin" + } + }, + "/teams/{tid}/features/fileSharing": { + "get": { + "description": " [internal route ID: (\"get\", FileSharingConfig)]\n\n", + "operationId": "get_FileSharingConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for fileSharing" + }, + "put": { + "description": " [internal route ID: (\"put\", FileSharingConfig)]\n\n", + "operationId": "put_FileSharingConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/FileSharingConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for fileSharing" + } + }, + "/teams/{tid}/features/legalhold": { + "get": { + "description": " [internal route ID: (\"get\", LegalholdConfig)]\n\n", + "operationId": "get_LegalholdConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for legalhold" + }, + "put": { + "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\n", + "operationId": "put_LegalholdConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LegalholdConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "too-large-team-for-legalhold", + "action-denied", + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Put config for legalhold" + } + }, + "/teams/{tid}/features/limitedEventFanout": { + "get": { + "description": " [internal route ID: (\"get\", LimitedEventFanoutConfig)]\n\n", + "operationId": "get_LimitedEventFanoutConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LimitedEventFanoutConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for limitedEventFanout" + } + }, + "/teams/{tid}/features/meetings": { + "get": { + "description": " [internal route ID: (\"get\", MeetingsConfig)]\n\n", + "operationId": "get_MeetingsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for meetings" + }, + "put": { + "description": " [internal route ID: (\"put\", MeetingsConfig)]\n\n", + "operationId": "put_MeetingsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for meetings" + } + }, + "/teams/{tid}/features/meetingsPremium": { + "get": { + "description": " [internal route ID: (\"get\", MeetingsPremiumConfig)]\n\n", + "operationId": "get_MeetingsPremiumConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsPremiumConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for meetingsPremium" + }, + "put": { + "description": " [internal route ID: (\"put\", MeetingsPremiumConfig)]\n\n", + "operationId": "put_MeetingsPremiumConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsPremiumConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsPremiumConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for meetingsPremium" + } + }, + "/teams/{tid}/features/mls": { + "get": { + "description": " [internal route ID: (\"get\", MLSConfigB)]\n\n", + "operationId": "get_MLSConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mls" + }, + "put": { + "description": " [internal route ID: (\"put\", MLSConfigB)]\n\n", + "operationId": "put_MLSConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mls" + } + }, + "/teams/{tid}/features/mlsE2EId": { + "get": { + "description": " [internal route ID: (\"get\", MlsE2EIdConfigB)]\n\n", + "operationId": "get_MlsE2EIdConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mlsE2EId" + }, + "put": { + "description": " [internal route ID: (\"put\", MlsE2EIdConfigB)]\n\n", + "operationId": "put_MlsE2EIdConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsE2EIdConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mlsE2EId" + } + }, + "/teams/{tid}/features/mlsMigration": { + "get": { + "description": " [internal route ID: (\"get\", MlsMigrationConfigB)]\n\n", + "operationId": "get_MlsMigrationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsMigration.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mlsMigration" + }, + "put": { + "description": " [internal route ID: (\"put\", MlsMigrationConfigB)]\n\n", + "operationId": "put_MlsMigrationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsMigration.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsMigration.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mlsMigration" + } + }, + "/teams/{tid}/features/outlookCalIntegration": { + "get": { + "description": " [internal route ID: (\"get\", OutlookCalIntegrationConfig)]\n\n", + "operationId": "get_OutlookCalIntegrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for outlookCalIntegration" + }, + "put": { + "description": " [internal route ID: (\"put\", OutlookCalIntegrationConfig)]\n\n", + "operationId": "put_OutlookCalIntegrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for outlookCalIntegration" + } + }, + "/teams/{tid}/features/searchVisibility": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityAvailableConfig)]\n\n", + "operationId": "get_SearchVisibilityAvailableConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for searchVisibility" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityAvailableConfig)]\n\n", + "operationId": "put_SearchVisibilityAvailableConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for searchVisibility" + } + }, + "/teams/{tid}/features/searchVisibilityInbound": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityInboundConfig)]\n\n", + "operationId": "get_SearchVisibilityInboundConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for searchVisibilityInbound" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityInboundConfig)]\n\n", + "operationId": "put_SearchVisibilityInboundConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for searchVisibilityInbound" + } + }, + "/teams/{tid}/features/selfDeletingMessages": { + "get": { + "description": " [internal route ID: (\"get\", SelfDeletingMessagesConfigB)]\n\n", + "operationId": "get_SelfDeletingMessagesConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for selfDeletingMessages" + }, + "put": { + "description": " [internal route ID: (\"put\", SelfDeletingMessagesConfigB)]\n\n", + "operationId": "put_SelfDeletingMessagesConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for selfDeletingMessages" + } + }, + "/teams/{tid}/features/simplifiedUserConnectionRequestQRCode": { + "get": { + "description": " [internal route ID: (\"get\", SimplifiedUserConnectionRequestQRCodeConfig)]\n\n", + "operationId": "get_SimplifiedUserConnectionRequestQRCodeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SimplifiedUserConnectionRequestQRCode.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for simplifiedUserConnectionRequestQRCode" + } + }, + "/teams/{tid}/features/sndFactorPasswordChallenge": { + "get": { + "description": " [internal route ID: (\"get\", SndFactorPasswordChallengeConfig)]\n\n", + "operationId": "get_SndFactorPasswordChallengeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for sndFactorPasswordChallenge" + }, + "put": { + "description": " [internal route ID: (\"put\", SndFactorPasswordChallengeConfig)]\n\n", + "operationId": "put_SndFactorPasswordChallengeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for sndFactorPasswordChallenge" + } + }, + "/teams/{tid}/features/sso": { + "get": { + "description": " [internal route ID: (\"get\", SSOConfig)]\n\n", + "operationId": "get_SSOConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SSOConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for sso" + } + }, + "/teams/{tid}/features/stealthUsers": { + "get": { + "description": " [internal route ID: (\"get\", StealthUsersConfig)]\n\n", + "operationId": "get_StealthUsersConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/StealthUsersConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for stealthUsers" + } + }, + "/teams/{tid}/features/validateSAMLemails": { + "get": { + "description": " [internal route ID: (\"get\", ValidateSAMLEmailsConfig)]\n\n", + "operationId": "get_ValidateSAMLEmailsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for validateSAMLemails" + } + }, + "/teams/{tid}/get-members-by-ids-using-post": { + "post": { + "description": " [internal route ID: \"get-team-members-by-ids\"]\n\nThe `has_more` field in the response body is always `false`.", + "operationId": "get-team-members-by-ids", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserIdList" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMemberList" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-uids", + "message": "Can only process 2000 user ids per request." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-uids" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `maxResults`\n\nCan only process 2000 user ids per request. (label: `too-many-uids`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get team members by user id list" + } + }, + "/teams/{tid}/invitations": { + "get": { + "description": " [internal route ID: \"get-team-invitations\"]\n\n", + "operationId": "get-team-invitations", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Invitation id to start from (ascending).", + "in": "query", + "name": "start", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Number of results to return (default 100, max 500).", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationList" + } + } + }, + "description": "List of sent invitations" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "List the sent team invitations" + }, + "post": { + "description": " [internal route ID: \"send-team-invitation\"]\n\nInvitations are sent by email. The maximum allowed number of pending team invitations is equal to the team size.", + "operationId": "send-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "description": "Invitation was created and sent.", + "headers": { + "Location": { + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions", + "too-many-team-invitations", + "blacklisted-email", + "no-identity", + "no-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)\n\nToo many team invitations for this team (label: `too-many-team-invitations`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nThe user has no verified email (label: `no-identity`)\n\nThis operation requires the user to have a verified email address. (label: `no-email`)" + } + }, + "summary": "Create and send a new team invitation." + } + }, + "/teams/{tid}/invitations/{iid}": { + "delete": { + "description": " [internal route ID: \"delete-team-invitation\"]\n\n", + "operationId": "delete-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Invitation deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Delete a pending team invitation by ID." + }, + "get": { + "description": " [internal route ID: \"get-team-invitation\"]\n\n", + "operationId": "get-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "description": "Invitation" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Notification not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Notification not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `iid` or Notification not found. (label: `not-found`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "duplicate-entry", + "message": "Entry already exists" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "duplicate-entry" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Entry already exists (label: `duplicate-entry`)" + } + }, + "summary": "Get a pending team invitation by ID." + } + }, + "/teams/{tid}/legalhold/consent": { + "post": { + "description": " [internal route ID: \"consent-to-legal-hold\"]\n\n", + "operationId": "consent-to-legal-hold", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Grant consent successful" + }, + "204": { + "description": "Consent already granted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Consent to legal hold" + } + }, + "/teams/{tid}/legalhold/settings": { + "delete": { + "description": " [internal route ID: \"delete-legal-hold-settings\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to members with a legalhold client (via brig)\n- UserLegalHoldDisabled event to contacts of members with a legalhold client (via brig)", + "operationId": "delete-legal-hold-settings", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveLegalHoldSettingsRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Legal hold service settings deleted" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "invalid-op", + "action-denied", + "no-team-member", + "operation-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Delete legal hold service settings" + }, + "get": { + "description": " [internal route ID: \"get-legal-hold-settings\"]\n\n", + "operationId": "get-legal-hold-settings", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get legal hold service settings" + }, + "post": { + "description": " [internal route ID: \"create-legal-hold-settings\"]\n\n", + "operationId": "create-legal-hold-settings", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewLegalHoldService" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService" + } + } + }, + "description": "Legal hold service settings created" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-status-bad", + "message": "legal hold service: invalid response" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-status-bad", + "legalhold-invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)\n\nlegal hold service pubkey is invalid (label: `legalhold-invalid-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Create legal hold service settings" + } + }, + "/teams/{tid}/legalhold/{uid}": { + "delete": { + "description": " [internal route ID: \"disable-legal-hold-for-user\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to the user owning the client (via brig)\n- UserLegalHoldDisabled event to contacts of the user owning the client (via brig)", + "operationId": "disable-legal-hold-for-user", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DisableLegalHoldForUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Disable legal hold successful" + }, + "204": { + "description": "Legal hold was not enabled" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "action-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Disable legal hold for user" + }, + "get": { + "description": " [internal route ID: \"get-legal-hold\"]\n\n", + "operationId": "get-legal-hold", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserLegalHoldStatusResponse" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Get legal hold status" + }, + "post": { + "description": " [internal route ID: \"request-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- LegalHoldClientRequested event to contacts of the user the device is requested for, if they didn't already have a legalhold client (via brig)", + "operationId": "request-legal-hold-device", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Request device successful" + }, + "204": { + "description": "Request device already pending" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered", + "legalhold-status-bad" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service has not been registered for this team (label: `legalhold-not-registered`)\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-legal-hold-not-allowed", + "message": "A user who is under legal-hold may not participate in MLS conversations" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-legal-hold-not-allowed", + "legalhold-no-consent", + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A user who is under legal-hold may not participate in MLS conversations (label: `mls-legal-hold-not-allowed`)\n\nuser has not given consent to using legal hold (label: `legalhold-no-consent`)\n\nlegal hold is already enabled for this user (label: `legalhold-already-enabled`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-illegal-op", + "message": "internal server error: inconsistent change of user's legalhold state" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-illegal-op", + "legalhold-internal" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "internal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)\n\nlegal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)" + } + }, + "summary": "Request legal hold device" + } + }, + "/teams/{tid}/legalhold/{uid}/approve": { + "put": { + "description": " [internal route ID: \"approve-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientAdded event to the user owning the client (via brig)\n- UserLegalHoldEnabled event to contacts of the user owning the client (via brig)\n- ClientRemoved event to the user, if removing old client due to max number (via brig)", + "operationId": "approve-legal-hold-device", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ApproveLegalHoldForUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Legal hold approved" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "no-team-member", + "action-denied", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "legalhold-no-device-allocated", + "message": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-device-allocated" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nno legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow. (label: `legalhold-no-device-allocated`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "legalhold-already-enabled", + "message": "legal hold is already enabled for this user" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is already enabled for this user (label: `legalhold-already-enabled`)" + }, + "412": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 412, + "label": "legalhold-not-pending", + "message": "legal hold cannot be approved without being in a pending state" + }, + "properties": { + "code": { + "enum": [ + 412 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-pending" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold cannot be approved without being in a pending state (label: `legalhold-not-pending`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Approve legal hold device" + } + }, + "/teams/{tid}/members": { + "get": { + "description": " [internal route ID: \"get-team-members\"]\n\n", + "operationId": "get-team-members", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Optional, when not specified, the first page will be returned.Every returned page contains a `pagingState`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMembersPage" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get team members" + }, + "put": { + "description": " [internal route ID: \"update-team-member\"]\n\n", + "operationId": "update-team-member", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewTeamMember" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "too-many-team-admins", + "invalid-permissions", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nMaximum number of admins per team reached (label: `too-many-team-admins`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Update an existing team member" + } + }, + "/teams/{tid}/members/csv": { + "get": { + "description": " [internal route ID: \"get-team-members-csv\"]\n\nThe endpoint returns data in chunked transfer encoding. Internal server errors might result in a failed transfer instead of a 500 response.", + "operationId": "get-team-members-csv", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/csv": {} + }, + "description": "CSV of team members" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "You do not have permission to access this resource" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "You do not have permission to access this resource (label: `access-denied`)" + } + }, + "summary": "Get all members of the team as a CSV file" + } + }, + "/teams/{tid}/members/{uid}": { + "delete": { + "description": " [internal route ID: \"delete-team-member\"]\n\n", + "operationId": "delete-team-member", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMemberDeleteData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "202": { + "description": "Team member scheduled for deletion" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam not found (label: `no-team`)\n\nTeam member not found (label: `no-team-member`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + } + }, + "summary": "Remove an existing team member" + }, + "get": { + "description": " [internal route ID: \"get-team-member\"]\n\n", + "operationId": "get-team-member", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMember" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Get single team member" + } + }, + "/teams/{tid}/search": { + "get": { + "description": " [internal route ID: \"browse-team\"]\n\n", + "operationId": "browse-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Search expression", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Role filter, eg. `member,partner`. Empty list means do not filter.", + "in": "query", + "name": "frole", + "required": false, + "schema": { + "items": { + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "type": "array" + } + }, + { + "description": "Can be one of name, handle, email, saml_idp, managed_by, role, created_at.", + "in": "query", + "name": "sortby", + "required": false, + "schema": { + "enum": [ + "name", + "handle", + "email", + "saml_idp", + "managed_by", + "role", + "created_at" + ], + "type": "string" + } + }, + { + "description": "Can be one of asc, desc.", + "in": "query", + "name": "sortorder", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default: 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Optional, when not specified, the first page will be returned. Every returned page contains a `paging_state`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Filter for (un-)verified email", + "in": "query", + "name": "email", + "required": false, + "schema": { + "enum": [ + "unverified", + "verified" + ], + "type": "string" + } + }, + { + "description": "Optional, return only non-searchable members when false.", + "in": "query", + "name": "searchable", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResult_TeamContact" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult_TeamContact" + } + } + }, + "description": "Search results" + } + }, + "summary": "Browse team for members (requires add-user permission)" + } + }, + "/teams/{tid}/search-visibility": { + "get": { + "description": " [internal route ID: \"get-search-visibility\"]\n\n", + "operationId": "get-search-visibility", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSearchVisibilityView" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Shows the value for search visibility" + }, + "put": { + "description": " [internal route ID: \"set-search-visibility\"]\n\n", + "operationId": "set-search-visibility", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSearchVisibilityView" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Search visibility set" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "team-search-visibility-not-enabled", + "message": "Custom search is not available for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "team-search-visibility-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Custom search is not available for this team (label: `team-search-visibility-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Sets the search visibility for the whole team" + } + }, + "/teams/{tid}/size": { + "get": { + "description": " [internal route ID: \"get-team-size\"]\n\nCan be out of sync by roughly the `refresh_interval` of the ES index.", + "operationId": "get-team-size", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamSize" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSize" + } + } + }, + "description": "Number of team members" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get the number of team members as an integer" + } + }, + "/time": { + "get": { + "description": " [internal route ID: \"get-server-time\"]\n\nReturns the current server time in UTC with seconds precision.", + "operationId": "get-server-time", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServerTime" + } + } + }, + "description": "" + } + }, + "summary": "Get the current server time" + } + }, + "/upgrade-personal-to-team": { + "post": { + "description": " [internal route ID: \"upgrade-personal-to-team\"]\n\n", + "operationId": "upgrade-personal-to-team", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BindingNewTeamUser" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserTeam" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateUserTeam" + } + } + }, + "description": "Team created" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "user-already-in-a-team", + "message": "Switching teams is not allowed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-already-in-a-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-already-in-a-team", + "message": "Switching teams is not allowed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-already-in-a-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Switching teams is not allowed (label: `user-already-in-a-team`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "User not found (label: `not-found`)" + } + }, + "summary": "Upgrade personal user to team owner" + } + }, + "/user-groups": { + "get": { + "description": " [internal route ID: \"get-user-groups\"]\n\n", + "operationId": "get-user-groups", + "parameters": [ + { + "description": "Search string", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "required": false, + "schema": { + "enum": [ + "name", + "created_at" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "sort_order", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "page_size", + "required": false, + "schema": { + "description": "integer from [1..500]", + "type": "number" + } + }, + { + "description": "`name` of the last seen user group, used to get the next page when sorting by name.", + "in": "query", + "name": "last_seen_name", + "required": false, + "schema": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + { + "description": "`created_at` field of the last seen user group, used to get the next page when sorting by created_at.", + "in": "query", + "name": "last_seen_created_at", + "required": false, + "schema": { + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + } + }, + { + "description": "`id` of the last seen group, used to get the next page. **Must** be sent to get the next page.", + "in": "query", + "name": "last_seen_id", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_channels", + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_member_count", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupPage" + } + } + }, + "description": "" + } + }, + "summary": "Fetch groups accessible to the logged-in user" + }, + "post": { + "description": " [internal route ID: \"create-user-group\"]\n\n", + "operationId": "create-user-group", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewUserGroup" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroup" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nOnly team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + } + } + } + }, + "/user-groups/check-name": { + "post": { + "description": " [internal route ID: \"check-user-group-name-available\"]\n\n", + "operationId": "check-user-group-name-available", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CheckUserGroupName" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGroupNameAvailability" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupNameAvailability" + } + } + }, + "description": "OK" + } + }, + "summary": "[STUB] Check if a user group name is available" + } + }, + "/user-groups/{gid}": { + "delete": { + "description": " [internal route ID: \"delete-user-group\"]\n\n", + "operationId": "delete-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User group deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + }, + "get": { + "description": " [internal route ID: \"get-user-group\"]\n\n", + "operationId": "get-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_channels", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGroup" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroup" + } + } + }, + "description": "User Group Found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` or User group not found (label: `user-group-not-found`)\n\nUser group not found (label: `user-group-not-found`)" + } + }, + "summary": "Fetch a group accessible to the logged-in user" + }, + "put": { + "description": " [internal route ID: \"update-user-group\"]\n\n", + "operationId": "update-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User added updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + } + }, + "/user-groups/{gid}/channels": { + "put": { + "description": " [internal route ID: \"update-user-group-channels\"]\n\n", + "operationId": "update-user-group-channels", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "append_only", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateUserGroupChannels" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User group channels updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + }, + "summary": "Replaces the channels with the given list." + } + }, + "/user-groups/{gid}/users": { + "post": { + "description": " [internal route ID: \"add-users-to-group-bulk\"]\n\n", + "operationId": "add-users-to-group-bulk", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupAddUsers" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Users added to group" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nOnly team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + }, + "put": { + "description": " [internal route ID: \"update-user-group-members\"]\n\n", + "operationId": "update-user-group-members", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateUserGroupMembers" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User group members updated" + } + }, + "summary": "[STUB] Update user group members. Replaces the users with the given list." + } + }, + "/user-groups/{gid}/users/{uid}": { + "delete": { + "description": " [internal route ID: \"remove-user-from-group\"]\n\n", + "operationId": "remove-user-from-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User removed from group" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` or `uid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + }, + "post": { + "description": " [internal route ID: \"add-user-to-group\"]\n\n", + "operationId": "add-user-to-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User added to group" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` or `uid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + } + }, + "/users/list-clients": { + "post": { + "description": " [internal route ID: \"list-clients-bulk@v2\"]\n\nIf a backend is unreachable, the clients from that backend will be omitted from the response", + "operationId": "list-clients-bulk@v2", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LimitedQualifiedUserIdList_500" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "qualified_user_map": { + "$ref": "#/components/schemas/QualifiedUserMap_Set_PubClient" + } + }, + "type": "object" + } + } + }, + "description": "" + } + }, + "summary": "List all clients for a set of user ids" + } + }, + "/users/list-prekeys": { + "post": { + "description": " [internal route ID: \"get-multi-user-prekey-bundle-qualified\"]\n\nYou can't request information for more users than maximum conversation size.", + "operationId": "get-multi-user-prekey-bundle-qualified", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QualifiedUserClients" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QualifiedUserClientPrekeyMapV4" + } + } + }, + "description": "" + } + }, + "summary": "(deprecated) Given a map of user IDs to client IDs return a prekey for each one." + } + }, + "/users/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-user-qualified\"]\n\n", + "operationId": "get-user-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserProfile" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserProfile" + } + } + }, + "description": "User found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`uid_domain` or `uid` or User not found (label: `not-found`)" + } + }, + "summary": "Get a user by Domain and UserId" + } + }, + "/users/{uid_domain}/{uid}/clients": { + "get": { + "description": " [internal route ID: \"get-user-clients-qualified\"]\n\n", + "operationId": "get-user-clients-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Get all of a user's clients" + } + }, + "/users/{uid_domain}/{uid}/clients/{client}": { + "get": { + "description": " [internal route ID: \"get-user-client-qualified\"]\n\n", + "operationId": "get-user-client-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PubClient" + } + } + }, + "description": "" + } + }, + "summary": "Get a specific client of a user" + } + }, + "/users/{uid_domain}/{uid}/prekeys": { + "get": { + "description": " [internal route ID: \"get-users-prekey-bundle-qualified\"]\n\n", + "operationId": "get-users-prekey-bundle-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PrekeyBundle" + } + } + }, + "description": "" + } + }, + "summary": "Get a prekey for each client of a user." + } + }, + "/users/{uid_domain}/{uid}/prekeys/{client}": { + "get": { + "description": " [internal route ID: \"get-users-prekeys-client-qualified\"]\n\n", + "operationId": "get-users-prekeys-client-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientPrekey" + } + } + }, + "description": "" + } + }, + "summary": "Get a prekey for a specific client of a user." + } + }, + "/users/{uid_domain}/{uid}/supported-protocols": { + "get": { + "description": " [internal route ID: \"get-supported-protocols\"]\n\n", + "operationId": "get-supported-protocols", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array", + "uniqueItems": true + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array", + "uniqueItems": true + } + } + }, + "description": "Protocols supported by the user" + } + }, + "summary": "Get a user's supported protocols" + } + }, + "/users/{uid}/email": { + "put": { + "description": " [internal route ID: \"update-user-email\"]\n\nIf the user has a pending email validation, the validation email will be resent.", + "operationId": "update-user-email", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Resend email address validation email." + } + }, + "/users/{uid}/rich-info": { + "get": { + "description": " [internal route ID: \"get-rich-info\"]\n\n", + "operationId": "get-rich-info", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RichInfoAssocList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RichInfoAssocList" + } + } + }, + "description": "Rich info about the user" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Get a user's rich info" + } + }, + "/users/{uid}/searchable": { + "post": { + "description": " [internal route ID: \"set-user-searchable\"]\n\n", + "operationId": "set-user-searchable", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SetSearchable" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Set user's visibility in search" + } + }, + "/verification-code/send": { + "post": { + "description": " [internal route ID: \"send-verification-code\"]\n\n", + "operationId": "send-verification-code", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendVerificationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Verification code sent." + } + }, + "summary": "Send a verification code to a given email address." + } + } + }, + "security": [ + { + "ZAuth": [] + } + ], + "servers": [ + { + "url": "/v14" + } + ] +} diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 4fcdefe883d..e4fbf8ed6c2 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -237,22 +237,23 @@ internalEndpointsSwaggerDocsAPIs = -- -- Dual to `internalEndpointsSwaggerDocsAPI`. versionedSwaggerDocsAPI :: Servant.Server VersionedSwaggerDocsAPI -versionedSwaggerDocsAPI (Just (VersionNumber V14)) = +versionedSwaggerDocsAPI (Just (VersionNumber V15)) = swaggerSchemaUIServer $ - ( serviceSwagger @VersionAPITag @'V14 - <> serviceSwagger @BrigAPITag @'V14 - <> serviceSwagger @GalleyAPITag @'V14 - <> serviceSwagger @SparAPITag @'V14 - <> serviceSwagger @CargoholdAPITag @'V14 - <> serviceSwagger @CannonAPITag @'V14 - <> serviceSwagger @GundeckAPITag @'V14 - <> serviceSwagger @ProxyAPITag @'V14 - <> serviceSwagger @OAuthAPITag @'V14 + ( serviceSwagger @VersionAPITag @'V15 + <> serviceSwagger @BrigAPITag @'V15 + <> serviceSwagger @GalleyAPITag @'V15 + <> serviceSwagger @SparAPITag @'V15 + <> serviceSwagger @CargoholdAPITag @'V15 + <> serviceSwagger @CannonAPITag @'V15 + <> serviceSwagger @GundeckAPITag @'V15 + <> serviceSwagger @ProxyAPITag @'V15 + <> serviceSwagger @OAuthAPITag @'V15 ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $((unTypeCode . embedText) =<< makeRelativeToProject "docs/swagger.md") - & S.servers .~ [S.Server ("/" <> toUrlPiece V14) Nothing mempty] + & S.servers .~ [S.Server ("/" <> toUrlPiece V15) Nothing mempty] & cleanupSwagger +versionedSwaggerDocsAPI (Just (VersionNumber V14)) = swaggerPregenUIServer $(pregenSwagger V14) versionedSwaggerDocsAPI (Just (VersionNumber V13)) = swaggerPregenUIServer $(pregenSwagger V13) versionedSwaggerDocsAPI (Just (VersionNumber V12)) = swaggerPregenUIServer $(pregenSwagger V12) versionedSwaggerDocsAPI (Just (VersionNumber V11)) = swaggerPregenUIServer $(pregenSwagger V11) From 4cbb17b81398edd971c59d77d0681b7ecff39666 Mon Sep 17 00:00:00 2001 From: Jan Schumacher <155645800+jschumacher-wire@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:30:39 +0100 Subject: [PATCH 56/60] nginx-ingress-services: enable RotationPolicy setting for cert key pinning (#4945) * nginx-ingress-services: enable RotationPolicy setting for cert key pinning * better var handling --- changelog.d/2-features/cert-rotation-policy | 11 +++++++++++ .../templates/certificate.yaml | 6 +++--- .../templates/certificate_federator.yaml | 2 +- charts/nginx-ingress-services/values.yaml | 12 ++++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 changelog.d/2-features/cert-rotation-policy diff --git a/changelog.d/2-features/cert-rotation-policy b/changelog.d/2-features/cert-rotation-policy new file mode 100644 index 00000000000..b5ce2813fd2 --- /dev/null +++ b/changelog.d/2-features/cert-rotation-policy @@ -0,0 +1,11 @@ +nginx-ingress-services chart: Add support for cert-manager Certificate +privateKey rotation policy configuration. This allows preserving private +keys across certificate renewals for client key pinning scenarios. + +Configuration options: +- `tls.privateKey.rotationPolicy` - for ingress certificates +- `federator.tls.privateKey.rotationPolicy` - for federator certificate + +Setting rotationPolicy to "Never" preserves the private key, enabling +scenarios where clients pin the server's public key rather than the +certificate itself. diff --git a/charts/nginx-ingress-services/templates/certificate.yaml b/charts/nginx-ingress-services/templates/certificate.yaml index 833fcec3265..8e88813fbdd 100644 --- a/charts/nginx-ingress-services/templates/certificate.yaml +++ b/charts/nginx-ingress-services/templates/certificate.yaml @@ -20,10 +20,10 @@ spec: secretName: {{ include "nginx-ingress-services.getCertificateSecretName" . | quote }} privateKey: - algorithm: ECDSA - size: 384 # 521 is not supported by Letsencrypt + algorithm: {{ .Values.tls.privateKey.algorithm }} + size: {{ .Values.tls.privateKey.size }} encoding: PKCS1 - rotationPolicy: Always + rotationPolicy: {{ .Values.tls.privateKey.rotationPolicy }} dnsNames: - {{ .Values.config.dns.https }} diff --git a/charts/nginx-ingress-services/templates/certificate_federator.yaml b/charts/nginx-ingress-services/templates/certificate_federator.yaml index 0ac26b6b2f1..1361ea386b5 100644 --- a/charts/nginx-ingress-services/templates/certificate_federator.yaml +++ b/charts/nginx-ingress-services/templates/certificate_federator.yaml @@ -29,7 +29,7 @@ spec: algorithm: ECDSA size: 256 # hs-tls only supports p256 encoding: PKCS1 - rotationPolicy: Always + rotationPolicy: {{ .Values.federator.tls.privateKey.rotationPolicy }} dnsNames: - "{{ or .Values.config.dns.certificateDomain .Values.config.dns.federator }}" {{- end -}} diff --git a/charts/nginx-ingress-services/values.yaml b/charts/nginx-ingress-services/values.yaml index d254733505f..8dc8608bb5e 100644 --- a/charts/nginx-ingress-services/values.yaml +++ b/charts/nginx-ingress-services/values.yaml @@ -15,6 +15,11 @@ fakeS3: federator: enabled: false integrationTestHelper: false + tls: + privateKey: + # rotationPolicy: Always (default) regenerates key on each renewal + # rotationPolicy: Never preserves key across renewals (for key pinning) + rotationPolicy: Always # If you want to use TLS termination on the ingress, # then set this variable to true and ensure that there # is a valid wildcard TLS certificate @@ -38,6 +43,13 @@ tls: # the validation depth between a federator client certificate and tlsClientCA verify_depth: 1 createIssuer: true + # Private key settings for cert-manager Certificate resources + privateKey: + # rotationPolicy: Always (default) regenerates key on each renewal + # rotationPolicy: Never preserves key across renewals (for key pinning) + rotationPolicy: Always + algorithm: ECDSA + size: 384 # 521 is not supported by Let's Encrypt issuer: # In a multi-domain backend (multi-ingress) setup, this name will be # augmented with the 'ingressName' (e.g. 'letsencrypt-http01-') From 8d3c7bb94ef0bb58e07e31c5a1b9199b3b299052 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 13 Jan 2026 11:24:33 +0100 Subject: [PATCH 57/60] WPB-22654 Update CellsInternal Feature Flag (#4940) --- changelog.d/2-features/WPB-22168 | 2 +- charts/galley/values.yaml | 2 +- docs/src/developer/reference/config-options.md | 2 +- hack/helm_vars/wire-server/values.yaml.gotmpl | 2 +- integration/test/Test/FeatureFlags/CellsInternal.hs | 4 ++-- integration/test/Test/FeatureFlags/Util.hs | 2 +- libs/wire-api/src/Wire/API/Team/Feature.hs | 4 ++-- services/brig/docs/swagger-v14.json | 4 ++-- services/galley/galley.integration.yaml | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/changelog.d/2-features/WPB-22168 b/changelog.d/2-features/WPB-22168 index 0d2e1e27003..75fdd9c366a 100644 --- a/changelog.d/2-features/WPB-22168 +++ b/changelog.d/2-features/WPB-22168 @@ -1 +1 @@ -New team feature config `cellsInternal` (#4889, #4907) +New team feature config `cellsInternal` (#4889, #4907, #4940) diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 578b559ae56..a8ac5e2b3b9 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -261,7 +261,7 @@ config: collabora: edition: COOL storage: - teamQuotaBytes: "1000000000000" + perUserQuotaBytes: "1000000000000" allowedGlobalOperations: status: enabled config: diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 6ce12e9bf15..d72b8158faa 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -662,7 +662,7 @@ config: collabora: edition: COOL storage: - teamQuotaBytes: "1000000000000" # 1 TB + perUserQuotaBytes: "1000000000000" # 1 TB ``` ### Allowed Global Operations diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index eb8c96576a0..490c70deb6b 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -378,7 +378,7 @@ galley: collabora: edition: COOL storage: - teamQuotaBytes: "1000000000000" + perUserQuotaBytes: "1000000000000" allowedGlobalOperations: status: enabled config: diff --git a/integration/test/Test/FeatureFlags/CellsInternal.hs b/integration/test/Test/FeatureFlags/CellsInternal.hs index fa7cef27f05..c202004789d 100644 --- a/integration/test/Test/FeatureFlags/CellsInternal.hs +++ b/integration/test/Test/FeatureFlags/CellsInternal.hs @@ -38,7 +38,7 @@ testCellsInternalEvent = do event %. "data.status" `shouldMatch` "enabled" event %. "data.config.backend.url" `shouldMatch` "https://cells-beta.wire.com" event %. "data.config.collabora.edition" `shouldMatch` "COOL" - event %. "data.config.storage.teamQuotaBytes" `shouldMatch` quota + event %. "data.config.storage.perUserQuotaBytes" `shouldMatch` quota testCellsInternal :: (HasCallStack) => App () testCellsInternal = do @@ -81,7 +81,7 @@ mkFt s ls c = .= object [ "backend" .= object ["url" .= c.url], "collabora" .= object ["edition" .= c.collabora], - "storage" .= object ["teamQuotaBytes" .= c.quota] + "storage" .= object ["perUserQuotaBytes" .= c.quota] ] ] diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 0f0cf793197..ce2acde064a 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -238,7 +238,7 @@ defAllFeatures = .= object [ "backend" .= object ["url" .= "https://cells-beta.wire.com"], "collabora" .= object ["edition" .= "COOL"], - "storage" .= object ["teamQuotaBytes" .= "1000000000000"] + "storage" .= object ["perUserQuotaBytes" .= "1000000000000"] ] ], "meetings" .= enabled, diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 99c07fe7b8a..5f560380d29 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1807,7 +1807,7 @@ instance ToSchema NumBytes where ) newtype CellsStorage = CellsStorage - { teamQuotaBytes :: NumBytes + { perUserQuotaBytes :: NumBytes } deriving (Show, Eq, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsStorage @@ -1817,7 +1817,7 @@ instance ToSchema CellsStorage where schema = object "CellsStorage" $ CellsStorage - <$> teamQuotaBytes .= field "teamQuotaBytes" schema + <$> perUserQuotaBytes .= field "perUserQuotaBytes" schema data CellsInternalConfigB t f = CellsInternalConfig { backend :: Wear t f CellsBackend, diff --git a/services/brig/docs/swagger-v14.json b/services/brig/docs/swagger-v14.json index 2b31a47a4be..c8a6bd595e5 100644 --- a/services/brig/docs/swagger-v14.json +++ b/services/brig/docs/swagger-v14.json @@ -1035,12 +1035,12 @@ }, "CellsStorage": { "properties": { - "teamQuotaBytes": { + "perUserQuotaBytes": { "type": "string" } }, "required": [ - "teamQuotaBytes" + "perUserQuotaBytes" ], "type": "object" }, diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 6d6369ff3d9..f4dce07f1b7 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -205,7 +205,7 @@ settings: collabora: edition: COOL storage: - teamQuotaBytes: "1000000000000" + perUserQuotaBytes: "1000000000000" allowedGlobalOperations: status: enabled config: From b9736b689b87c0a313c26be92d7b0f140fbcaa48 Mon Sep 17 00:00:00 2001 From: Zebot Date: Tue, 13 Jan 2026 11:30:15 +0000 Subject: [PATCH 58/60] Add changelog for Release 2026-01-13 --- CHANGELOG.md | 127 ++++++++++++++++++ changelog.d/0-release-notes/WPB-22170 | 1 - changelog.d/1-api-changes/ WPB-22702 | 1 - changelog.d/1-api-changes/WPB-22170 | 1 - ...dd-app-fields-category-description-creator | 1 - .../1-api-changes/add-get-app-endpoint | 1 - .../1-api-changes/add-scim-group-pagination | 1 - changelog.d/2-features/WPB-21964 | 3 - changelog.d/2-features/WPB-22168 | 1 - changelog.d/2-features/WPB-22170 | 1 - changelog.d/2-features/cert-rotation-policy | 11 -- changelog.d/2-features/faster-migration | 10 -- .../2-features/log-errors-in-conv-migration | 7 - .../mls-skip-error-for-broken-groups | 1 - .../2-features/multi-ingress-idp-domains | 9 -- changelog.d/3-bug-fixes/WPB-21706 | 1 - changelog.d/3-bug-fixes/WPB-22101.md | 1 - ...PB-22154-fix-move-user-between-scim-tokens | 1 - .../WPB-22287-fix-saml-xml-headers | 1 - ...297-Fix-ToSchema-instance-for-SearchResult | 1 - changelog.d/3-bug-fixes/conv-code-doc-fix | 2 - changelog.d/3-bug-fixes/conv-gc-grace-period | 3 - changelog.d/3-bug-fixes/make-apps-findable | 1 - changelog.d/3-bug-fixes/mls-message-epoch-0 | 1 - .../3-bug-fixes/optimize-conv-member-queries | 1 - .../provide-rabbitmq-for-brig-nonfederated | 4 - changelog.d/5-internal/WPB-16262 | 1 - changelog.d/5-internal/WPB-22515 | 1 - changelog.d/5-internal/WPB-22577 | 1 - changelog.d/5-internal/active-users-no-mls | 1 - changelog.d/5-internal/add-IdP-golden-test | 1 - .../explain-MultiIngressSSO-test-helpers | 1 - changelog.d/5-internal/flake | 1 - 33 files changed, 127 insertions(+), 73 deletions(-) delete mode 100644 changelog.d/0-release-notes/WPB-22170 delete mode 100644 changelog.d/1-api-changes/ WPB-22702 delete mode 100644 changelog.d/1-api-changes/WPB-22170 delete mode 100644 changelog.d/1-api-changes/add-app-fields-category-description-creator delete mode 100644 changelog.d/1-api-changes/add-get-app-endpoint delete mode 100644 changelog.d/1-api-changes/add-scim-group-pagination delete mode 100644 changelog.d/2-features/WPB-21964 delete mode 100644 changelog.d/2-features/WPB-22168 delete mode 100644 changelog.d/2-features/WPB-22170 delete mode 100644 changelog.d/2-features/cert-rotation-policy delete mode 100644 changelog.d/2-features/faster-migration delete mode 100644 changelog.d/2-features/log-errors-in-conv-migration delete mode 100644 changelog.d/2-features/mls-skip-error-for-broken-groups delete mode 100644 changelog.d/2-features/multi-ingress-idp-domains delete mode 100644 changelog.d/3-bug-fixes/WPB-21706 delete mode 100644 changelog.d/3-bug-fixes/WPB-22101.md delete mode 100644 changelog.d/3-bug-fixes/WPB-22154-fix-move-user-between-scim-tokens delete mode 100644 changelog.d/3-bug-fixes/WPB-22287-fix-saml-xml-headers delete mode 100644 changelog.d/3-bug-fixes/WPB-22297-Fix-ToSchema-instance-for-SearchResult delete mode 100644 changelog.d/3-bug-fixes/conv-code-doc-fix delete mode 100644 changelog.d/3-bug-fixes/conv-gc-grace-period delete mode 100644 changelog.d/3-bug-fixes/make-apps-findable delete mode 100644 changelog.d/3-bug-fixes/mls-message-epoch-0 delete mode 100644 changelog.d/3-bug-fixes/optimize-conv-member-queries delete mode 100644 changelog.d/3-bug-fixes/provide-rabbitmq-for-brig-nonfederated delete mode 100644 changelog.d/5-internal/WPB-16262 delete mode 100644 changelog.d/5-internal/WPB-22515 delete mode 100644 changelog.d/5-internal/WPB-22577 delete mode 100644 changelog.d/5-internal/active-users-no-mls delete mode 100644 changelog.d/5-internal/add-IdP-golden-test delete mode 100644 changelog.d/5-internal/explain-MultiIngressSSO-test-helpers delete mode 100644 changelog.d/5-internal/flake diff --git a/CHANGELOG.md b/CHANGELOG.md index fe56aa54cce..10a4365e8fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,130 @@ +# [2026-01-13] (Chart Release 5.25.0) + +## Release notes + + +* Operators: if you override `galley.settings.featureFlags.cells` in your Helm values, update your override to include the newly required cells config fields (channels/groups/one2one/users/collabora/publicLinks/storage/metadata); if you use the chart defaults, no action is needed. (#4903) + + +## API changes + + +* Create new API version V15 and finalize API version V14 (#4942) + +* The `PUT /teams/:tid/features/cells` endpoint has changed in API version V14 and requires additional config values. (#4903) + +* Add new fields to apps: category, description, creator (#4879) + +* Add "get app" endpoint to Brig (`GET /teams/:tid/apps/:id`) (#4879) + +* Add [pagination to SCIM groups](https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.4) in Spar /scim/v2/Groups + + +## Features + + +* Add `meetingsPremium` feature flag to distinguish premium teams from trial teams. Meetings created by premium team members are marked as non-trial. Public endpoints: GET/PUT /teams/:tid/features/meetingsPremium. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetingsPremium and lock status management. + + Add `meetings` feature flag to control access to the meetings API. When disabled, all meetings endpoints return 403 Forbidden. The feature is enabled and unlocked by default. Public endpoints: GET/PUT /teams/:tid/features/meetings. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetings and lock status management. (#PR_NOT_FOUND) + +* New team feature config `cellsInternal` (#4889, #4907, #4940) + +* The `cells` feature flag now contains a set of additional configuration values (#4903) + +* nginx-ingress-services chart: Add support for cert-manager Certificate + privateKey rotation policy configuration. This allows preserving private + keys across certificate renewals for client key pinning scenarios. + + Configuration options: + - `tls.privateKey.rotationPolicy` - for ingress certificates + - `federator.tls.privateKey.rotationPolicy` - for federator certificate + + Setting rotationPolicy to "Never" preserves the private key, enabling + scenarios where clients pin the server's public key rather than the + certificate itself. (#4945) + +* Allow configuring page size and parallelism for conversation migration to + PostgreSQL. This can be configured like this: + + ```yaml + background-worker: + config: + migrateConversationsOptions: + pageSize: 10000 + parallelism: 2 + ``` (#4904) + +* Introduce new metrics for better tracking of conversation migration to postgresql: + 1. `wire_local_convs_migration_failed` + 2. `wire_user_remote_convs_migration_failed` + + If any of these become `1`, it means the migration has failed. The logs would + contain the error. In order to restart the migration, the background-worker must + be restarted. (#4891) + +* Commits with a broken group info are now let through if the group was already broken (#4883) + +* When a SAML IdP is created on a multi-ingress domain (implying that + multi-ingress domains are configured in Spar) the domain is added as `domain` + field to that IdP's `extraInfo` (`WireIdP` type in Haskell.) To avoid confusion + in later lookups, at most one IdP can be configured per multi-ingress domain. + If multi-ingress is not configured or it's not configured for the specific + domain, no `domain` field gets added to the IdP. This guards against creating + multiple IdPs and then assigning them to multi-ingress domains. Thus, users who + don't use multi-ingress don't observe any change. This feature only opens the + door to later provide an IdP for a multi-ingress domain. (#4778) + + +## Bug fixes and other updates + + +* Fixed notification endpoint returning an empty page with `hasMore=true` (#4871) + +* Fix SCIM groups endpoint to only return SCIM-managed groups, not wire-managed groups (#4906) + +* Fixed: change user idp, external_id or emails via scim (scim user update / patch failed to update parts of `ValidScimId`). (#4887) + +* Add `` to SAML/XML output. (#4898) + +* Make Swagger schema instances for `GET /search/results` and `GET /teams/{tid}/search` distinct (#4921) + +* Fix swagger docs for `GET` and `POST` on `/conversations/{cnv}/code` to show + that the response will always include the `uri` field. (#4911) + +* Reduce gc_grace_period for all conversation related tables to 1 day. This will + help restart the postgresql migration after a day, if it fails mid way. Lowering + it too much runs the risk of offline nodes resurrecting deleted data. (#4899) + +* Make underlying users for apps findable from `GET /search/contacts` (#4920) + +* Reject messages in MLS groups while in epoch 0. (#4811) + +* Optimize Postgresql queries for getting conversation members (#4896, #4896) + +* Since 5.23.23 (5866babe26f6b49511320dedb5b58a289ddcdbd4) RabbitMQ settings are + mandatory for Brig in both, federated and non-federated setups. Unfortunately, + this wasn't reflected in Brig's Helm chart. So, non-federated deployments were + failing. (#4886) + + +## Internal changes + + +* Upgrade nixpkgs and dependencies (icluding GHC from 9.8 to 9.10) (#4909) + +* Upgrade ormolu to match GHC 9.10. (#4923) + +* Fix postgres migrations on CI test runs (#4931) + +* Add `mls-users` tool to list all active users that don't support MLS. (#4888) + +* Add a golden test for `IdP` (de-) serialization to ensure the format doesn't change due to future developments. (#4927) + +* Explain MultiIngressSSO test helper functions a bit better. (#4882) + +* Use nix flakes instead of niv and manually pinned git dependencies (#4933) + + # [2025-11-26] (Chart Release 5.24.0) ## Release notes diff --git a/changelog.d/0-release-notes/WPB-22170 b/changelog.d/0-release-notes/WPB-22170 deleted file mode 100644 index d5b1b991432..00000000000 --- a/changelog.d/0-release-notes/WPB-22170 +++ /dev/null @@ -1 +0,0 @@ -Operators: if you override `galley.settings.featureFlags.cells` in your Helm values, update your override to include the newly required cells config fields (channels/groups/one2one/users/collabora/publicLinks/storage/metadata); if you use the chart defaults, no action is needed. diff --git a/changelog.d/1-api-changes/ WPB-22702 b/changelog.d/1-api-changes/ WPB-22702 deleted file mode 100644 index d8806e9870d..00000000000 --- a/changelog.d/1-api-changes/ WPB-22702 +++ /dev/null @@ -1 +0,0 @@ -Create new API version V15 and finalize API version V14 diff --git a/changelog.d/1-api-changes/WPB-22170 b/changelog.d/1-api-changes/WPB-22170 deleted file mode 100644 index fce4d607389..00000000000 --- a/changelog.d/1-api-changes/WPB-22170 +++ /dev/null @@ -1 +0,0 @@ -The `PUT /teams/:tid/features/cells` endpoint has changed in API version V14 and requires additional config values. diff --git a/changelog.d/1-api-changes/add-app-fields-category-description-creator b/changelog.d/1-api-changes/add-app-fields-category-description-creator deleted file mode 100644 index 31cb223bed0..00000000000 --- a/changelog.d/1-api-changes/add-app-fields-category-description-creator +++ /dev/null @@ -1 +0,0 @@ -Add new fields to apps: category, description, creator \ No newline at end of file diff --git a/changelog.d/1-api-changes/add-get-app-endpoint b/changelog.d/1-api-changes/add-get-app-endpoint deleted file mode 100644 index e433e9cc22e..00000000000 --- a/changelog.d/1-api-changes/add-get-app-endpoint +++ /dev/null @@ -1 +0,0 @@ -Add "get app" endpoint to Brig (`GET /teams/:tid/apps/:id`) \ No newline at end of file diff --git a/changelog.d/1-api-changes/add-scim-group-pagination b/changelog.d/1-api-changes/add-scim-group-pagination deleted file mode 100644 index a31aa7dc0f6..00000000000 --- a/changelog.d/1-api-changes/add-scim-group-pagination +++ /dev/null @@ -1 +0,0 @@ -Add [pagination to SCIM groups](https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.4) in Spar /scim/v2/Groups \ No newline at end of file diff --git a/changelog.d/2-features/WPB-21964 b/changelog.d/2-features/WPB-21964 deleted file mode 100644 index 3e01c82a61f..00000000000 --- a/changelog.d/2-features/WPB-21964 +++ /dev/null @@ -1,3 +0,0 @@ -Add `meetingsPremium` feature flag to distinguish premium teams from trial teams. Meetings created by premium team members are marked as non-trial. Public endpoints: GET/PUT /teams/:tid/features/meetingsPremium. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetingsPremium and lock status management. - -Add `meetings` feature flag to control access to the meetings API. When disabled, all meetings endpoints return 403 Forbidden. The feature is enabled and unlocked by default. Public endpoints: GET/PUT /teams/:tid/features/meetings. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetings and lock status management. diff --git a/changelog.d/2-features/WPB-22168 b/changelog.d/2-features/WPB-22168 deleted file mode 100644 index 75fdd9c366a..00000000000 --- a/changelog.d/2-features/WPB-22168 +++ /dev/null @@ -1 +0,0 @@ -New team feature config `cellsInternal` (#4889, #4907, #4940) diff --git a/changelog.d/2-features/WPB-22170 b/changelog.d/2-features/WPB-22170 deleted file mode 100644 index 617aff42949..00000000000 --- a/changelog.d/2-features/WPB-22170 +++ /dev/null @@ -1 +0,0 @@ -The `cells` feature flag now contains a set of additional configuration values diff --git a/changelog.d/2-features/cert-rotation-policy b/changelog.d/2-features/cert-rotation-policy deleted file mode 100644 index b5ce2813fd2..00000000000 --- a/changelog.d/2-features/cert-rotation-policy +++ /dev/null @@ -1,11 +0,0 @@ -nginx-ingress-services chart: Add support for cert-manager Certificate -privateKey rotation policy configuration. This allows preserving private -keys across certificate renewals for client key pinning scenarios. - -Configuration options: -- `tls.privateKey.rotationPolicy` - for ingress certificates -- `federator.tls.privateKey.rotationPolicy` - for federator certificate - -Setting rotationPolicy to "Never" preserves the private key, enabling -scenarios where clients pin the server's public key rather than the -certificate itself. diff --git a/changelog.d/2-features/faster-migration b/changelog.d/2-features/faster-migration deleted file mode 100644 index 412c24b5920..00000000000 --- a/changelog.d/2-features/faster-migration +++ /dev/null @@ -1,10 +0,0 @@ -Allow configuring page size and parallelism for conversation migration to -PostgreSQL. This can be configured like this: - -```yaml -background-worker: - config: - migrateConversationsOptions: - pageSize: 10000 - parallelism: 2 -``` diff --git a/changelog.d/2-features/log-errors-in-conv-migration b/changelog.d/2-features/log-errors-in-conv-migration deleted file mode 100644 index aba4edb77c9..00000000000 --- a/changelog.d/2-features/log-errors-in-conv-migration +++ /dev/null @@ -1,7 +0,0 @@ -Introduce new metrics for better tracking of conversation migration to postgresql: -1. `wire_local_convs_migration_failed` -2. `wire_user_remote_convs_migration_failed` - -If any of these become `1`, it means the migration has failed. The logs would -contain the error. In order to restart the migration, the background-worker must -be restarted. \ No newline at end of file diff --git a/changelog.d/2-features/mls-skip-error-for-broken-groups b/changelog.d/2-features/mls-skip-error-for-broken-groups deleted file mode 100644 index 629198a71e2..00000000000 --- a/changelog.d/2-features/mls-skip-error-for-broken-groups +++ /dev/null @@ -1 +0,0 @@ -Commits with a broken group info are now let through if the group was already broken diff --git a/changelog.d/2-features/multi-ingress-idp-domains b/changelog.d/2-features/multi-ingress-idp-domains deleted file mode 100644 index f8944f2006c..00000000000 --- a/changelog.d/2-features/multi-ingress-idp-domains +++ /dev/null @@ -1,9 +0,0 @@ -When a SAML IdP is created on a multi-ingress domain (implying that -multi-ingress domains are configured in Spar) the domain is added as `domain` -field to that IdP's `extraInfo` (`WireIdP` type in Haskell.) To avoid confusion -in later lookups, at most one IdP can be configured per multi-ingress domain. -If multi-ingress is not configured or it's not configured for the specific -domain, no `domain` field gets added to the IdP. This guards against creating -multiple IdPs and then assigning them to multi-ingress domains. Thus, users who -don't use multi-ingress don't observe any change. This feature only opens the -door to later provide an IdP for a multi-ingress domain. diff --git a/changelog.d/3-bug-fixes/WPB-21706 b/changelog.d/3-bug-fixes/WPB-21706 deleted file mode 100644 index 81a14f6352c..00000000000 --- a/changelog.d/3-bug-fixes/WPB-21706 +++ /dev/null @@ -1 +0,0 @@ -Fixed notification endpoint returning an empty page with `hasMore=true` diff --git a/changelog.d/3-bug-fixes/WPB-22101.md b/changelog.d/3-bug-fixes/WPB-22101.md deleted file mode 100644 index bba88783a70..00000000000 --- a/changelog.d/3-bug-fixes/WPB-22101.md +++ /dev/null @@ -1 +0,0 @@ -Fix SCIM groups endpoint to only return SCIM-managed groups, not wire-managed groups diff --git a/changelog.d/3-bug-fixes/WPB-22154-fix-move-user-between-scim-tokens b/changelog.d/3-bug-fixes/WPB-22154-fix-move-user-between-scim-tokens deleted file mode 100644 index 4ff779c2447..00000000000 --- a/changelog.d/3-bug-fixes/WPB-22154-fix-move-user-between-scim-tokens +++ /dev/null @@ -1 +0,0 @@ -Fixed: change user idp, external_id or emails via scim (scim user update / patch failed to update parts of `ValidScimId`). \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/WPB-22287-fix-saml-xml-headers b/changelog.d/3-bug-fixes/WPB-22287-fix-saml-xml-headers deleted file mode 100644 index a030675108f..00000000000 --- a/changelog.d/3-bug-fixes/WPB-22287-fix-saml-xml-headers +++ /dev/null @@ -1 +0,0 @@ -Add `` to SAML/XML output. \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/WPB-22297-Fix-ToSchema-instance-for-SearchResult b/changelog.d/3-bug-fixes/WPB-22297-Fix-ToSchema-instance-for-SearchResult deleted file mode 100644 index 266490f0a2c..00000000000 --- a/changelog.d/3-bug-fixes/WPB-22297-Fix-ToSchema-instance-for-SearchResult +++ /dev/null @@ -1 +0,0 @@ -Make Swagger schema instances for `GET /search/results` and `GET /teams/{tid}/search` distinct \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/conv-code-doc-fix b/changelog.d/3-bug-fixes/conv-code-doc-fix deleted file mode 100644 index 8bafb87673f..00000000000 --- a/changelog.d/3-bug-fixes/conv-code-doc-fix +++ /dev/null @@ -1,2 +0,0 @@ -Fix swagger docs for `GET` and `POST` on `/conversations/{cnv}/code` to show -that the response will always include the `uri` field. \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/conv-gc-grace-period b/changelog.d/3-bug-fixes/conv-gc-grace-period deleted file mode 100644 index 8fe81db89d8..00000000000 --- a/changelog.d/3-bug-fixes/conv-gc-grace-period +++ /dev/null @@ -1,3 +0,0 @@ -Reduce gc_grace_period for all conversation related tables to 1 day. This will -help restart the postgresql migration after a day, if it fails mid way. Lowering -it too much runs the risk of offline nodes resurrecting deleted data. \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/make-apps-findable b/changelog.d/3-bug-fixes/make-apps-findable deleted file mode 100644 index 38173b19c94..00000000000 --- a/changelog.d/3-bug-fixes/make-apps-findable +++ /dev/null @@ -1 +0,0 @@ -Make underlying users for apps findable from `GET /search/contacts` \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/mls-message-epoch-0 b/changelog.d/3-bug-fixes/mls-message-epoch-0 deleted file mode 100644 index 723e838f4b7..00000000000 --- a/changelog.d/3-bug-fixes/mls-message-epoch-0 +++ /dev/null @@ -1 +0,0 @@ -Reject messages in MLS groups while in epoch 0. diff --git a/changelog.d/3-bug-fixes/optimize-conv-member-queries b/changelog.d/3-bug-fixes/optimize-conv-member-queries deleted file mode 100644 index 8125a7bbdd3..00000000000 --- a/changelog.d/3-bug-fixes/optimize-conv-member-queries +++ /dev/null @@ -1 +0,0 @@ -Optimize Postgresql queries for getting conversation members (#4896, ##) \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/provide-rabbitmq-for-brig-nonfederated b/changelog.d/3-bug-fixes/provide-rabbitmq-for-brig-nonfederated deleted file mode 100644 index 3efd9944787..00000000000 --- a/changelog.d/3-bug-fixes/provide-rabbitmq-for-brig-nonfederated +++ /dev/null @@ -1,4 +0,0 @@ -Since 5.23.23 (5866babe26f6b49511320dedb5b58a289ddcdbd4) RabbitMQ settings are -mandatory for Brig in both, federated and non-federated setups. Unfortunately, -this wasn't reflected in Brig's Helm chart. So, non-federated deployments were -failing. diff --git a/changelog.d/5-internal/WPB-16262 b/changelog.d/5-internal/WPB-16262 deleted file mode 100644 index 75aeef95efa..00000000000 --- a/changelog.d/5-internal/WPB-16262 +++ /dev/null @@ -1 +0,0 @@ -Upgrade nixpkgs and dependencies (icluding GHC from 9.8 to 9.10) diff --git a/changelog.d/5-internal/WPB-22515 b/changelog.d/5-internal/WPB-22515 deleted file mode 100644 index c2acd72b4b7..00000000000 --- a/changelog.d/5-internal/WPB-22515 +++ /dev/null @@ -1 +0,0 @@ -Upgrade ormolu to match GHC 9.10. diff --git a/changelog.d/5-internal/WPB-22577 b/changelog.d/5-internal/WPB-22577 deleted file mode 100644 index 145fc336176..00000000000 --- a/changelog.d/5-internal/WPB-22577 +++ /dev/null @@ -1 +0,0 @@ -Fix postgres migrations on CI test runs diff --git a/changelog.d/5-internal/active-users-no-mls b/changelog.d/5-internal/active-users-no-mls deleted file mode 100644 index fa33cb7a9fe..00000000000 --- a/changelog.d/5-internal/active-users-no-mls +++ /dev/null @@ -1 +0,0 @@ -Add `mls-users` tool to list all active users that don't support MLS. diff --git a/changelog.d/5-internal/add-IdP-golden-test b/changelog.d/5-internal/add-IdP-golden-test deleted file mode 100644 index 3da9806f2b0..00000000000 --- a/changelog.d/5-internal/add-IdP-golden-test +++ /dev/null @@ -1 +0,0 @@ -Add a golden test for `IdP` (de-) serialization to ensure the format doesn't change due to future developments. diff --git a/changelog.d/5-internal/explain-MultiIngressSSO-test-helpers b/changelog.d/5-internal/explain-MultiIngressSSO-test-helpers deleted file mode 100644 index 35f0bccb841..00000000000 --- a/changelog.d/5-internal/explain-MultiIngressSSO-test-helpers +++ /dev/null @@ -1 +0,0 @@ -Explain MultiIngressSSO test helper functions a bit better. diff --git a/changelog.d/5-internal/flake b/changelog.d/5-internal/flake deleted file mode 100644 index aaba7fcf9f8..00000000000 --- a/changelog.d/5-internal/flake +++ /dev/null @@ -1 +0,0 @@ -Use nix flakes instead of niv and manually pinned git dependencies \ No newline at end of file From 458301b4e113de77e0f7b611aed83f66a50ca564 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 13 Jan 2026 15:29:44 +0000 Subject: [PATCH 59/60] fixed changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10a4365e8fe..60a69f4547f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ * Add `meetingsPremium` feature flag to distinguish premium teams from trial teams. Meetings created by premium team members are marked as non-trial. Public endpoints: GET/PUT /teams/:tid/features/meetingsPremium. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetingsPremium and lock status management. - Add `meetings` feature flag to control access to the meetings API. When disabled, all meetings endpoints return 403 Forbidden. The feature is enabled and unlocked by default. Public endpoints: GET/PUT /teams/:tid/features/meetings. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetings and lock status management. (#PR_NOT_FOUND) + Add `meetings` feature flag to control access to the meetings API. When disabled, all meetings endpoints return 403 Forbidden. The feature is enabled and unlocked by default. Public endpoints: GET/PUT /teams/:tid/features/meetings. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetings and lock status management. (#4915) * New team feature config `cellsInternal` (#4889, #4907, #4940) From 071eb692146679d78dc8e60c87fcea99d3565b6d Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 14 Jan 2026 08:47:57 +0000 Subject: [PATCH 60/60] fix changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60a69f4547f..dbfc9d778c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,8 @@ migrateConversationsOptions: pageSize: 10000 parallelism: 2 - ``` (#4904) + ``` + (#4904) * Introduce new metrics for better tracking of conversation migration to postgresql: 1. `wire_local_convs_migration_failed`