From 38840fd08b5f61406a7b6f9b91db7309b3c48fc4 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 18 Feb 2025 16:56:36 +0100 Subject: [PATCH 01/25] Make it possible to parse configs from DB --- libs/schema-profunctor/src/Data/Schema.hs | 3 + libs/wire-api/src/Wire/API/Team/Feature.hs | 444 +++++++++++++----- .../Golden/Generated/LockableFeature_team.hs | 12 +- libs/wire-api/wire-api.cabal | 1 + .../galley/src/Galley/API/Teams/Features.hs | 4 +- .../src/Galley/API/Teams/Features/Get.hs | 4 +- .../src/Galley/Cassandra/MakeFeature.hs | 23 +- services/galley/test/integration/API/Teams.hs | 2 +- .../integration/API/Teams/LegalHold/Util.hs | 2 +- 9 files changed, 346 insertions(+), 149 deletions(-) diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index 87fdee9075e..36d5287a7ce 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -285,14 +285,17 @@ schemaOut (SchemaP _ _ (SchemaOut o)) = o class (Functor f) => FieldFunctor doc f where parseFieldF :: (A.Value -> A.Parser a) -> A.Object -> Text -> A.Parser (f a) + extractF :: (Monoid w) => SchemaP doc v w a b -> SchemaP doc v w (f a) b mkDocF :: doc -> doc instance FieldFunctor doc Identity where parseFieldF f obj key = Identity <$> A.explicitParseField f obj (Key.fromText key) + extractF = lmap runIdentity mkDocF = id instance (HasOpt doc) => FieldFunctor doc Maybe where parseFieldF f obj key = A.explicitParseFieldMaybe f obj (Key.fromText key) + extractF = maybe_ mkDocF = mkOpt -- | A schema for a one-field JSON object. diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index d088e895117..829485fb210 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1,6 +1,8 @@ {-# LANGUAGE ApplicativeDo #-} +{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE StrictData #-} +{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. @@ -25,11 +27,14 @@ module Wire.API.Team.Feature featureName, featureNameBS, LockStatus (..), + DbConfig (..), DbFeature (..), dbFeatureLockStatus, dbFeatureStatus, dbFeatureConfig, dbFeatureModConfig, + featureToDB, + resolveDbFeature, LockableFeature (..), defUnlockedFeature, defLockedFeature, @@ -42,8 +47,8 @@ module Wire.API.Team.Feature FeatureTTL' (..), FeatureTTLUnit (..), EnforceAppLock (..), - genericComputeFeature, IsFeatureConfig (..), + ParseDbFeature (..), FeatureSingleton (..), DeprecatedFeatureName, LockStatusResponse (..), @@ -61,12 +66,17 @@ module Wire.API.Team.Feature SndFactorPasswordChallengeConfig (..), SearchVisibilityInboundConfig (..), ClassifiedDomainsConfig (..), - AppLockConfig (..), + AppLockConfig, + AppLockConfigB (..), FileSharingConfig (..), - MLSConfig (..), + MLSConfigB (..), + MLSConfig, OutlookCalIntegrationConfig (..), - MlsE2EIdConfig (..), - MlsMigrationConfig (..), + UseProxyOnMobile (..), + MlsE2EIdConfigB (..), + MlsE2EIdConfig, + MlsMigrationConfigB (..), + MlsMigrationConfig, EnforceFileDownloadLocationConfig (..), LimitedEventFanoutConfig (..), DomainRegistrationConfig (..), @@ -77,10 +87,15 @@ module Wire.API.Team.Feature NpUpdate (..), npUpdate, AllTeamFeatures, + parseDbFeature, + mkAllFeatures, ) where +import Barbies +import Barbies.Bare import Cassandra.CQL qualified as Cass +import Control.Applicative import Control.Lens ((?~)) import Data.Aeson qualified as A import Data.Aeson.Types qualified as A @@ -94,28 +109,32 @@ import Data.Either.Extra (maybeToEither) import Data.Id import Data.Json.Util import Data.Kind +import Data.Map qualified as M import Data.Misc (HttpsUrl) -import Data.Monoid +import Data.Monoid hiding (All, First) import Data.OpenApi qualified as S import Data.Proxy import Data.SOP import Data.Schema import Data.Scientific (toBoundedInteger) +import Data.Semigroup hiding (All) import Data.Text qualified as T 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.Time import Deriving.Aeson import GHC.TypeLits -import Imports +import Generics.SOP qualified as GSOP +import Imports hiding (All, First) import Servant (FromHttpApiData (..), ToHttpApiData (..)) import Test.QuickCheck (getPrintableString) import Test.QuickCheck.Arbitrary (arbitrary) import Test.QuickCheck.Gen (suchThat) import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite -import Wire.API.Routes.Named +import Wire.API.Routes.Named hiding (unnamed) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) ---------------------------------------------------------------------- @@ -236,6 +255,9 @@ newtype DbFeature cfg = DbFeature {applyDbFeature :: LockableFeature cfg -> LockableFeature cfg} deriving (Semigroup, Monoid) via Endo (LockableFeature cfg) +instance Default (DbFeature cfg) where + def = mempty + dbFeatureLockStatus :: LockStatus -> DbFeature cfg dbFeatureLockStatus s = DbFeature $ \w -> w {lockStatus = s} @@ -248,6 +270,30 @@ dbFeatureConfig c = DbFeature $ \w -> w {config = c} dbFeatureModConfig :: (cfg -> cfg) -> DbFeature cfg dbFeatureModConfig f = DbFeature $ \w -> w {config = f w.config} +featureToDB :: LockableFeature cfg -> DbFeature cfg +featureToDB = DbFeature . const + +resolveDbFeature :: LockableFeature cfg -> DbFeature cfg -> LockableFeature cfg +resolveDbFeature defFeature dbFeature = + let feat = applyDbFeature dbFeature defFeature + in case feat.lockStatus of + LockStatusLocked -> defFeature {lockStatus = LockStatusLocked} + LockStatusUnlocked -> feat + +newtype DbConfig = DbConfig {unDbConfig :: A.Value} + deriving (Eq, Show) + +instance Default DbConfig where + def = DbConfig (A.object []) + +instance Cass.Cql DbConfig where + ctype = Cass.Tagged Cass.TextColumn + + fromCql (Cass.CqlText t) = fmap DbConfig . A.eitherDecode' . TL.encodeUtf8 . TL.fromStrict $ t + fromCql _ = Left "service key pem: blob expected" + + toCql (DbConfig c) = Cass.CqlText . TL.toStrict . TL.decodeUtf8 . A.encode $ c + ---------------------------------------------------------------------- -- LockableFeature @@ -534,26 +580,14 @@ instance ToSchema LockStatusResponse where LockStatusResponse <$> _unlockStatus .= field "lockStatus" schema --- | Convert a feature coming from the database to its public form. This can be --- overridden on a feature basis by implementing the `computeFeature` method of --- the `GetFeatureConfig` class. -genericComputeFeature :: forall cfg. LockableFeature cfg -> DbFeature cfg -> LockableFeature cfg -genericComputeFeature defFeature dbFeature = - let feat = applyDbFeature dbFeature defFeature - in case feat.lockStatus of - LockStatusLocked -> defFeature {lockStatus = LockStatusLocked} - LockStatusUnlocked -> feat - -------------------------------------------------------------------------------- -- GuestLinks feature data GuestLinksConfig = GuestLinksConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform GuestLinksConfig) deriving (RenderableSymbol) via (RenderableTypeName GuestLinksConfig) - -instance Default GuestLinksConfig where - def = GuestLinksConfig + deriving (ParseDbFeature, Default) via TrivialFeature GuestLinksConfig instance ToSchema GuestLinksConfig where schema = object "GuestLinksConfig" objectSchema @@ -571,12 +605,10 @@ instance IsFeatureConfig GuestLinksConfig where -- Legalhold feature data LegalholdConfig = LegalholdConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform LegalholdConfig) deriving (RenderableSymbol) via (RenderableTypeName LegalholdConfig) - -instance Default LegalholdConfig where - def = LegalholdConfig + deriving (ParseDbFeature, Default) via TrivialFeature LegalholdConfig instance Default (LockableFeature LegalholdConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -594,12 +626,10 @@ instance ToSchema LegalholdConfig where -- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data SSOConfig = SSOConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform SSOConfig) deriving (RenderableSymbol) via (RenderableTypeName SSOConfig) - -instance Default SSOConfig where - def = SSOConfig + deriving (ParseDbFeature, Default) via TrivialFeature SSOConfig instance Default (LockableFeature SSOConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -618,12 +648,10 @@ instance ToSchema SSOConfig where -- | Wether a team is allowed to change search visibility -- See the handle of PUT /teams/:tid/search-visibility data SearchVisibilityAvailableConfig = SearchVisibilityAvailableConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform SearchVisibilityAvailableConfig) deriving (RenderableSymbol) via (RenderableTypeName SearchVisibilityAvailableConfig) - -instance Default SearchVisibilityAvailableConfig where - def = SearchVisibilityAvailableConfig + deriving (ParseDbFeature, Default) via TrivialFeature SearchVisibilityAvailableConfig instance Default (LockableFeature SearchVisibilityAvailableConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -643,12 +671,10 @@ type instance DeprecatedFeatureName SearchVisibilityAvailableConfig = "search-vi -- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data ValidateSAMLEmailsConfig = ValidateSAMLEmailsConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform ValidateSAMLEmailsConfig) deriving (RenderableSymbol) via (RenderableTypeName ValidateSAMLEmailsConfig) - -instance Default ValidateSAMLEmailsConfig where - def = ValidateSAMLEmailsConfig + deriving (ParseDbFeature, Default) via (TrivialFeature ValidateSAMLEmailsConfig) instance ToSchema ValidateSAMLEmailsConfig where schema = object "ValidateSAMLEmailsConfig" objectSchema @@ -668,12 +694,10 @@ type instance DeprecatedFeatureName ValidateSAMLEmailsConfig = "validate-saml-em -- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data DigitalSignaturesConfig = DigitalSignaturesConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform DigitalSignaturesConfig) deriving (RenderableSymbol) via (RenderableTypeName DigitalSignaturesConfig) - -instance Default DigitalSignaturesConfig where - def = DigitalSignaturesConfig + deriving (ParseDbFeature, Default) via (TrivialFeature DigitalSignaturesConfig) instance Default (LockableFeature DigitalSignaturesConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -717,12 +741,10 @@ instance Cass.Cql One2OneCalls where data ConferenceCallingConfig = ConferenceCallingConfig { one2OneCalls :: One2OneCalls } - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform ConferenceCallingConfig) deriving (RenderableSymbol) via (RenderableTypeName ConferenceCallingConfig) - -instance Default ConferenceCallingConfig where - def = ConferenceCallingConfig {one2OneCalls = def} + deriving (ParseDbFeature, Default) via (SimpleFeature ConferenceCallingConfig) instance Default (LockableFeature ConferenceCallingConfig) where def = defLockedFeature {status = FeatureStatusEnabled} @@ -745,12 +767,10 @@ instance ToSchema ConferenceCallingConfig where -- SndFactorPasswordChallenge feature data SndFactorPasswordChallengeConfig = SndFactorPasswordChallengeConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform SndFactorPasswordChallengeConfig) deriving (RenderableSymbol) via (RenderableTypeName SndFactorPasswordChallengeConfig) - -instance Default SndFactorPasswordChallengeConfig where - def = SndFactorPasswordChallengeConfig + deriving (ParseDbFeature, Default) via (TrivialFeature SndFactorPasswordChallengeConfig) instance ToSchema SndFactorPasswordChallengeConfig where schema = object "SndFactorPasswordChallengeConfig" objectSchema @@ -767,13 +787,11 @@ instance IsFeatureConfig SndFactorPasswordChallengeConfig where -- SearchVisibilityInbound feature data SearchVisibilityInboundConfig = SearchVisibilityInboundConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform SearchVisibilityInboundConfig) deriving (S.ToSchema) via Schema SearchVisibilityInboundConfig deriving (RenderableSymbol) via (RenderableTypeName SearchVisibilityInboundConfig) - -instance Default SearchVisibilityInboundConfig where - def = SearchVisibilityInboundConfig + deriving (ParseDbFeature, Default) via (TrivialFeature SearchVisibilityInboundConfig) instance Default (LockableFeature SearchVisibilityInboundConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -799,6 +817,9 @@ data ClassifiedDomainsConfig = ClassifiedDomainsConfig deriving (ToJSON, FromJSON, S.ToSchema) via (Schema ClassifiedDomainsConfig) deriving (RenderableSymbol) via (RenderableTypeName ClassifiedDomainsConfig) +instance ParseDbFeature ClassifiedDomainsConfig where + parseDbConfig _ = fail "ClassifiedDomainsConfig cannot be parsed from the DB" + instance Default ClassifiedDomainsConfig where def = ClassifiedDomainsConfig [] @@ -822,24 +843,39 @@ instance IsFeatureConfig ClassifiedDomainsConfig where ---------------------------------------------------------------------- -- AppLock feature -data AppLockConfig = AppLockConfig - { applockEnforceAppLock :: EnforceAppLock, - applockInactivityTimeoutSecs :: Int32 +data AppLockConfigB t f = AppLockConfig + { enforce :: Wear t f EnforceAppLock, + timeout :: Wear t f Int32 } - deriving stock (Eq, Show, Generic) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AppLockConfig) - deriving (Arbitrary) via (GenericUniform AppLockConfig) - deriving (RenderableSymbol) via (RenderableTypeName AppLockConfig) + deriving (Generic, BareB) + +instance FunctorB (AppLockConfigB Covered) + +instance ApplicativeB (AppLockConfigB Covered) + +type AppLockConfig = AppLockConfigB Bare Identity + +deriving instance Eq AppLockConfig + +deriving instance Show AppLockConfig + +deriving via RenderableTypeName AppLockConfig instance RenderableSymbol AppLockConfig + +deriving via (GenericUniform AppLockConfig) instance Arbitrary AppLockConfig + +deriving via (BarbieFeature AppLockConfigB) instance ParseDbFeature AppLockConfig + +deriving via (BarbieFeature AppLockConfigB) instance ToSchema AppLockConfig instance Default AppLockConfig where def = AppLockConfig (EnforceAppLock False) 60 -instance ToSchema AppLockConfig where +instance (FieldFunctor SwaggerDoc f) => ToSchema (AppLockConfigB Covered f) where schema = object "AppLockConfig" $ AppLockConfig - <$> applockEnforceAppLock .= field "enforceAppLock" schema - <*> applockInactivityTimeoutSecs .= field "inactivityTimeoutSecs" schema + <$> (.enforce) .= extractF (fieldF "enforceAppLock" schema) + <*> (.timeout) .= extractF (fieldF "inactivityTimeoutSecs" schema) instance Default (LockableFeature AppLockConfig) where def = defUnlockedFeature @@ -862,9 +898,10 @@ instance ToSchema EnforceAppLock where -- FileSharing feature data FileSharingConfig = FileSharingConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform FileSharingConfig) deriving (RenderableSymbol) via (RenderableTypeName FileSharingConfig) + deriving (ParseDbFeature) via (TrivialFeature FileSharingConfig) instance Default FileSharingConfig where def = FileSharingConfig @@ -883,13 +920,14 @@ instance ToSchema FileSharingConfig where ---------------------------------------------------------------------- -- SelfDeletingMessagesConfig -newtype SelfDeletingMessagesConfig = SelfDeletingMessagesConfig +data SelfDeletingMessagesConfig = SelfDeletingMessagesConfig { sdmEnforcedTimeoutSeconds :: Int32 } - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema SelfDeletingMessagesConfig) deriving (Arbitrary) via (GenericUniform SelfDeletingMessagesConfig) deriving (RenderableSymbol) via (RenderableTypeName SelfDeletingMessagesConfig) + deriving (ParseDbFeature) via (SimpleFeature SelfDeletingMessagesConfig) instance Default SelfDeletingMessagesConfig where def = SelfDeletingMessagesConfig 0 @@ -911,16 +949,32 @@ instance IsFeatureConfig SelfDeletingMessagesConfig where ---------------------------------------------------------------------- -- MLSConfig -data MLSConfig = MLSConfig - { mlsProtocolToggleUsers :: [UserId], - mlsDefaultProtocol :: ProtocolTag, - mlsAllowedCipherSuites :: [CipherSuiteTag], - mlsDefaultCipherSuite :: CipherSuiteTag, - mlsSupportedProtocols :: [ProtocolTag] +data MLSConfigB t f = MLSConfig + { mlsProtocolToggleUsers :: Wear t f [UserId], + mlsDefaultProtocol :: Wear t f ProtocolTag, + mlsAllowedCipherSuites :: Wear t f [CipherSuiteTag], + mlsDefaultCipherSuite :: Wear t f CipherSuiteTag, + mlsSupportedProtocols :: Wear t f [ProtocolTag] } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform MLSConfig) - deriving (RenderableSymbol) via (RenderableTypeName MLSConfig) + deriving (Generic, BareB) + +deriving instance FunctorB (MLSConfigB Covered) + +deriving instance ApplicativeB (MLSConfigB Covered) + +type MLSConfig = MLSConfigB Bare Identity + +deriving instance Eq MLSConfig + +deriving instance Show MLSConfig + +deriving via (RenderableTypeName GuestLinksConfig) instance (RenderableSymbol MLSConfig) + +deriving via (GenericUniform MLSConfig) instance (Arbitrary MLSConfig) + +deriving via (BarbieFeature MLSConfigB) instance (ParseDbFeature MLSConfig) + +deriving via (BarbieFeature MLSConfigB) instance (ToSchema MLSConfig) instance Default MLSConfig where def = @@ -931,15 +985,15 @@ instance Default MLSConfig where MLS_128_DHKEMP256_AES128GCM_SHA256_P256 [ProtocolProteusTag, ProtocolMLSTag] -instance ToSchema MLSConfig where +instance (FieldFunctor SwaggerDoc f) => ToSchema (MLSConfigB Covered f) where schema = object "MLSConfig" $ MLSConfig - <$> mlsProtocolToggleUsers .= fieldWithDocModifier "protocolToggleUsers" (S.description ?~ "allowlist of users that may change protocols") (array schema) - <*> mlsDefaultProtocol .= field "defaultProtocol" schema - <*> mlsAllowedCipherSuites .= field "allowedCipherSuites" (array schema) - <*> mlsDefaultCipherSuite .= field "defaultCipherSuite" schema - <*> mlsSupportedProtocols .= field "supportedProtocols" (array schema) + <$> mlsProtocolToggleUsers .= extractF (fieldWithDocModifierF "protocolToggleUsers" (S.description ?~ "allowlist of users that may change protocols") (array schema)) + <*> mlsDefaultProtocol .= extractF (fieldF "defaultProtocol" schema) + <*> mlsAllowedCipherSuites .= extractF (fieldF "allowedCipherSuites" (array schema)) + <*> mlsDefaultCipherSuite .= extractF (fieldF "defaultCipherSuite" schema) + <*> mlsSupportedProtocols .= extractF (fieldF "supportedProtocols" (array schema)) instance Default (LockableFeature MLSConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -953,12 +1007,10 @@ instance IsFeatureConfig MLSConfig where -- ExposeInvitationURLsToTeamAdminConfig data ExposeInvitationURLsToTeamAdminConfig = ExposeInvitationURLsToTeamAdminConfig - deriving stock (Show, Eq, Generic) + deriving (Show, Eq, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform ExposeInvitationURLsToTeamAdminConfig) deriving (RenderableSymbol) via (RenderableTypeName ExposeInvitationURLsToTeamAdminConfig) - -instance Default ExposeInvitationURLsToTeamAdminConfig where - def = ExposeInvitationURLsToTeamAdminConfig + deriving (Default, ParseDbFeature) via (TrivialFeature ExposeInvitationURLsToTeamAdminConfig) instance Default (LockableFeature ExposeInvitationURLsToTeamAdminConfig) where def = defLockedFeature @@ -977,12 +1029,10 @@ instance ToSchema ExposeInvitationURLsToTeamAdminConfig where -- | This feature setting only applies to the Outlook Calendar extension for Wire. -- As it is an external service, it should only be configured through this feature flag and otherwise ignored by the backend. data OutlookCalIntegrationConfig = OutlookCalIntegrationConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform OutlookCalIntegrationConfig) deriving (RenderableSymbol) via (RenderableTypeName OutlookCalIntegrationConfig) - -instance Default OutlookCalIntegrationConfig where - def = OutlookCalIntegrationConfig + deriving (Default, ParseDbFeature) via (TrivialFeature OutlookCalIntegrationConfig) instance Default (LockableFeature OutlookCalIntegrationConfig) where def = defLockedFeature @@ -996,37 +1046,67 @@ instance ToSchema OutlookCalIntegrationConfig where schema = object "OutlookCalIntegrationConfig" objectSchema ---------------------------------------------------------------------- --- MlsE2EId - -data MlsE2EIdConfig = MlsE2EIdConfig - { verificationExpiration :: NominalDiffTime, - acmeDiscoveryUrl :: Maybe HttpsUrl, - crlProxy :: Maybe HttpsUrl, - useProxyOnMobile :: Bool +-- MlsE2EIdConfig + +newtype UseProxyOnMobile = UseProxyOnMobile {unUseProxyOnMobile :: Bool} + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform UseProxyOnMobile) + deriving (Semigroup) via First Bool + deriving (ToSchema) via Bool + +-- | This instance is necessary to derive ApplicativeB below, but it isn't +-- actually used. +instance Monoid UseProxyOnMobile where + mempty = error "Internal error: UseProxyOnMobile is not a monoid" + +instance Default UseProxyOnMobile where + def = UseProxyOnMobile False + +data MlsE2EIdConfigB t f = MlsE2EIdConfig + { verificationExpiration :: Wear t f NominalDiffTime, + acmeDiscoveryUrl :: Alt Maybe HttpsUrl, + crlProxy :: Alt Maybe HttpsUrl, + useProxyOnMobile :: UseProxyOnMobile } - deriving stock (Eq, Show, Generic) - deriving (RenderableSymbol) via (RenderableTypeName MlsE2EIdConfig) + deriving (BareB, Generic) + +deriving instance FunctorB (MlsE2EIdConfigB Covered) + +deriving instance ApplicativeB (MlsE2EIdConfigB Covered) + +type MlsE2EIdConfig = MlsE2EIdConfigB Bare Identity + +deriving via (RenderableTypeName MlsE2EIdConfig) instance (RenderableSymbol MlsE2EIdConfig) + +deriving via (BarbieFeature MlsE2EIdConfigB) instance ParseDbFeature MlsE2EIdConfig + +deriving via (BarbieFeature MlsE2EIdConfigB) instance ToSchema MlsE2EIdConfig + +deriving instance Eq MlsE2EIdConfig + +deriving instance Show MlsE2EIdConfig instance Default MlsE2EIdConfig where - def = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) Nothing Nothing False + def = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) empty empty def instance Arbitrary MlsE2EIdConfig where arbitrary = MlsE2EIdConfig <$> (fromIntegral <$> (arbitrary @Word32)) <*> arbitrary - <*> fmap Just arbitrary + <*> fmap (Alt . pure) arbitrary <*> arbitrary -instance ToSchema MlsE2EIdConfig where - schema :: ValueSchema NamedSwaggerDoc MlsE2EIdConfig +instance (FieldFunctor SwaggerDoc f) => ToSchema (MlsE2EIdConfigB Covered f) where schema = object "MlsE2EIdConfig" $ MlsE2EIdConfig - <$> (toSeconds . verificationExpiration) .= fieldWithDocModifier "verificationExpiration" veDesc (fromSeconds <$> schema) - <*> acmeDiscoveryUrl .= maybe_ (optField "acmeDiscoveryUrl" schema) - <*> crlProxy .= maybe_ (optField "crlProxy" schema) - <*> useProxyOnMobile .= (fromMaybe False <$> optField "useProxyOnMobile" schema) + <$> (fmap toSeconds . verificationExpiration) + .= extractF (fieldWithDocModifierF "verificationExpiration" veDesc (fromSeconds <$> schema)) + <*> (getAlt . acmeDiscoveryUrl) + .= fmap Alt (maybe_ (optField "acmeDiscoveryUrl" schema)) + <*> (getAlt . crlProxy) .= fmap Alt (maybe_ (optField "crlProxy" schema)) + <*> useProxyOnMobile .= fmap (fromMaybe def) (optField "useProxyOnMobile" schema) where fromSeconds :: Int -> NominalDiffTime fromSeconds = fromIntegral @@ -1060,12 +1140,27 @@ instance IsFeatureConfig MlsE2EIdConfig where ---------------------------------------------------------------------- -- MlsMigration -data MlsMigrationConfig = MlsMigrationConfig - { startTime :: Maybe UTCTime, - finaliseRegardlessAfter :: Maybe UTCTime +data MlsMigrationConfigB t f = MlsMigrationConfig + { startTime :: Wear t f (Maybe UTCTime), + finaliseRegardlessAfter :: Wear t f (Maybe UTCTime) } - deriving stock (Eq, Show, Generic) - deriving (RenderableSymbol) via (RenderableTypeName MlsMigrationConfig) + deriving (BareB, Generic) + +deriving instance FunctorB (MlsMigrationConfigB Covered) + +deriving instance ApplicativeB (MlsMigrationConfigB Covered) + +type MlsMigrationConfig = MlsMigrationConfigB Bare Identity + +deriving instance Eq MlsMigrationConfig + +deriving instance Show MlsMigrationConfig + +deriving via (BarbieFeature MlsMigrationConfigB) instance (ParseDbFeature MlsMigrationConfig) + +deriving via (BarbieFeature MlsMigrationConfigB) instance (ToSchema MlsMigrationConfig) + +deriving via (RenderableTypeName MlsMigrationConfig) instance (RenderableSymbol MlsMigrationConfig) instance Default MlsMigrationConfig where def = MlsMigrationConfig Nothing Nothing @@ -1080,12 +1175,25 @@ instance Arbitrary MlsMigrationConfig where finaliseRegardlessAfter = finaliseRegardlessAfter } -instance ToSchema MlsMigrationConfig where +-- | This class enables non-standard JSON instances for the Identity case of +-- this feature. For backwards compatibility, we need to make the two fields +-- optional even in the Identity case. A missing field gets parsed as +-- `Nothing`. Whereas with the default instance, they would be rejected. +class NestedMaybeFieldFunctor f where + nestedMaybeField :: Text -> ValueSchema SwaggerDoc a -> ObjectSchema SwaggerDoc (f (Maybe a)) + +instance NestedMaybeFieldFunctor Maybe where + nestedMaybeField name sch = maybe_ (optField name (nullable sch)) + +instance NestedMaybeFieldFunctor Identity where + nestedMaybeField name sch = Identity <$> runIdentity .= maybe_ (optField name sch) + +instance (NestedMaybeFieldFunctor f) => ToSchema (MlsMigrationConfigB Covered f) where schema = object "MlsMigration" $ MlsMigrationConfig - <$> startTime .= maybe_ (optField "startTime" utcTimeSchema) - <*> finaliseRegardlessAfter .= maybe_ (optField "finaliseRegardlessAfter" utcTimeSchema) + <$> startTime .= nestedMaybeField "startTime" (unnamed utcTimeSchema) + <*> finaliseRegardlessAfter .= nestedMaybeField "finaliseRegardlessAfter" (unnamed utcTimeSchema) instance Default (LockableFeature MlsMigrationConfig) where def = defLockedFeature @@ -1101,11 +1209,9 @@ instance IsFeatureConfig MlsMigrationConfig where data EnforceFileDownloadLocationConfig = EnforceFileDownloadLocationConfig { enforcedDownloadLocation :: Maybe Text } - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (RenderableSymbol) via (RenderableTypeName EnforceFileDownloadLocationConfig) - -instance Default EnforceFileDownloadLocationConfig where - def = EnforceFileDownloadLocationConfig Nothing + deriving (Default, ParseDbFeature) via (SimpleFeature EnforceFileDownloadLocationConfig) instance Arbitrary EnforceFileDownloadLocationConfig where arbitrary = EnforceFileDownloadLocationConfig . fmap (T.pack . getPrintableString) <$> arbitrary @@ -1133,12 +1239,10 @@ instance IsFeatureConfig EnforceFileDownloadLocationConfig where -- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data LimitedEventFanoutConfig = LimitedEventFanoutConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform LimitedEventFanoutConfig) deriving (RenderableSymbol) via (RenderableTypeName LimitedEventFanoutConfig) - -instance Default LimitedEventFanoutConfig where - def = LimitedEventFanoutConfig + deriving (Default, ParseDbFeature) via (TrivialFeature LimitedEventFanoutConfig) instance Default (LockableFeature LimitedEventFanoutConfig) where def = defUnlockedFeature @@ -1156,12 +1260,10 @@ instance ToSchema LimitedEventFanoutConfig where -- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data DomainRegistrationConfig = DomainRegistrationConfig - deriving stock (Eq, Show, Generic) + deriving (Eq, Show, Generic, GSOP.Generic) deriving (Arbitrary) via (GenericUniform DomainRegistrationConfig) deriving (RenderableSymbol) via (RenderableTypeName DomainRegistrationConfig) - -instance Default DomainRegistrationConfig where - def = DomainRegistrationConfig + deriving (Default, ParseDbFeature) via (TrivialFeature DomainRegistrationConfig) instance ToSchema DomainRegistrationConfig where schema = object "DomainRegistrationConfig" objectSchema @@ -1341,3 +1443,91 @@ deriving via (Schema AllTeamFeatures) instance (FromJSON AllTeamFeatures) deriving via (Schema AllTeamFeatures) instance (ToJSON AllTeamFeatures) deriving via (Schema AllTeamFeatures) instance (S.ToSchema AllTeamFeatures) + +-------------------------------------------------------------------------------- +-- DB feature parsing + +-- [Note: default values for configuration fields] +-- +-- When reading values for configuration types with multiple fields, we fall +-- back to default values for each field independently, instead of treating the +-- whole configuration as a single value that can be set or not. +-- +-- In most cases, either strategy would produce the same result, because there +-- is no way to set only *some* fields using the public API. However, that can +-- happen when a feature flag changes over time and gains new fields, as it has +-- been the case for mlsE2EId. +-- +-- Therefore, we use the first strategy consistently for all feature flags, +-- even when it does not matter. + +class ParseDbFeature cfg where + parseDbConfig :: DbConfig -> A.Parser (cfg -> cfg) + +newtype TrivialFeature cfg = TrivialFeature cfg + +instance (GSOP.IsProductType cfg '[]) => ParseDbFeature (TrivialFeature cfg) where + parseDbConfig _ = pure id + +instance (GSOP.IsProductType cfg '[]) => Default (TrivialFeature cfg) where + def = TrivialFeature (GSOP.productTypeTo Nil) + +newtype SimpleFeature cfg = SimpleFeature cfg + +instance (GSOP.IsWrappedType cfg a, ToSchema cfg) => ParseDbFeature (SimpleFeature cfg) where + parseDbConfig (DbConfig v) = do + config <- schemaParseJSON v + pure . const $ SimpleFeature config + +instance (GSOP.IsWrappedType cfg a, Default a) => Default (SimpleFeature cfg) where + def = SimpleFeature (GSOP.wrappedTypeTo def) + +newtype BarbieFeature b = BarbieFeature {unBarbieFeature :: b Bare Identity} + +instance + ( BareB b, + ApplicativeB (b Covered), + ToSchema (b Covered Maybe) + ) => + ParseDbFeature (BarbieFeature b) + where + parseDbConfig (DbConfig v) = do + cfg <- schemaParseJSON v + pure $ \(BarbieFeature defCfg) -> BarbieFeature (applyConfig defCfg cfg) + where + f :: Maybe a -> Identity a -> Identity a + f m (Identity x) = Identity $ fromMaybe x m + + applyConfig :: b Bare Identity -> b Covered Maybe -> b Bare Identity + applyConfig cfg1 cfg2 = bstrip $ bzipWith f cfg2 (bcover cfg1) + +instance (BareB b, ToSchema (b Covered Identity)) => ToSchema (BarbieFeature b) where + schema = (bcover . unBarbieFeature) .= fmap (BarbieFeature . bstrip) schema + +parseDbFeature :: + forall cfg. + (ParseDbFeature cfg) => + Maybe FeatureStatus -> + Maybe LockStatus -> + Maybe DbConfig -> + A.Parser (DbFeature cfg) +parseDbFeature status lockStatus dbConfig = + do + f <- maybe (pure id) parseDbConfig dbConfig + pure $ + foldMap dbFeatureStatus status + <> foldMap dbFeatureLockStatus lockStatus + <> dbFeatureModConfig f + +-- | Convert a map indexed by feature name to an NP value. +mkAllFeatures :: + forall cfgs a. + (Default a, All IsFeatureConfig cfgs) => + Map Text a -> + NP (K a) cfgs +mkAllFeatures m = + hmap (mapKK (fromMaybe def)) $ + hcpure (Proxy @IsFeatureConfig) get + where + get :: forall cfg. (IsFeatureConfig cfg) => K (Maybe a) cfg + get = K $ M.lookup (featureName @cfg) m diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs index 8c4f9562f39..2cfb3a4cdbd 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs @@ -83,9 +83,9 @@ testObject_LockableFeature_team_18 = LockStatusLocked ( MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) - Nothing - (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") - False + mempty + (Alt (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com")) + (UseProxyOnMobile False) ) parseHttpsUrl :: ByteString -> Either String HttpsUrl @@ -98,7 +98,7 @@ testObject_LockableFeature_team_19 = LockStatusLocked ( MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) - (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") - Nothing - True + (Alt (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com")) + mempty + (UseProxyOnMobile True) ) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 974dd9c4987..e269aba60ae 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -262,6 +262,7 @@ library , aeson >=2.0.1.0 , asn1-encoding , attoparsec >=0.10 + , barbies , base >=4 && <5 , base64-bytestring >=1.0 , binary diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 014990948f4..25cb0d37815 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -374,7 +374,7 @@ instance SetFeatureConfig AppLockConfig where type SetFeatureForTeamConstraints AppLockConfig r = Member (Error TeamFeatureError) r prepareFeature _tid feat = do - when ((applockInactivityTimeoutSecs feat.config) < 30) $ + when (feat.config.timeout < 30) $ throw AppLockInactivityTimeoutTooLow pure feat @@ -425,7 +425,7 @@ guardMlsE2EIdConfig :: Feature MlsE2EIdConfig -> Sem r a guardMlsE2EIdConfig handler uid tid feat = do - when (isNothing feat.config.crlProxy) $ throw MLSE2EIDMissingCrlProxy + when (isNothing (getAlt feat.config.crlProxy)) $ throw MLSE2EIDMissingCrlProxy handler uid tid feat instance SetFeatureConfig MlsMigrationConfig where diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index 7680c3e87b6..a85fff4353b 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -109,7 +109,7 @@ class Sem r (LockableFeature cfg) computeFeature _tid defFeature dbFeature = pure $ - genericComputeFeature @cfg defFeature dbFeature + resolveDbFeature @cfg defFeature dbFeature getFeature :: forall cfg r. @@ -381,7 +381,7 @@ instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where allowList <- input <&> view (settings . exposeInvitationURLsTeamAllowlist . to (fromMaybe [])) let teamAllowed = tid `elem` allowList lockStatus = if teamAllowed then LockStatusUnlocked else LockStatusLocked - pure $ genericComputeFeature defFeature (dbFeatureLockStatus lockStatus <> dbFeature) + pure $ resolveDbFeature defFeature (dbFeatureLockStatus lockStatus <> dbFeature) instance GetFeatureConfig OutlookCalIntegrationConfig diff --git a/services/galley/src/Galley/Cassandra/MakeFeature.hs b/services/galley/src/Galley/Cassandra/MakeFeature.hs index fea187b6096..de915c32ca0 100644 --- a/services/galley/src/Galley/Cassandra/MakeFeature.hs +++ b/services/galley/src/Galley/Cassandra/MakeFeature.hs @@ -109,14 +109,14 @@ instance MakeFeature AppLockConfig where <> dbFeatureModConfig ( \defCfg -> AppLockConfig - (fromMaybe defCfg.applockEnforceAppLock enforce) - (fromMaybe defCfg.applockInactivityTimeoutSecs timeout) + (fromMaybe defCfg.enforce enforce) + (fromMaybe defCfg.timeout timeout) ) featureToRow feat = Just feat.status - :* Just feat.config.applockEnforceAppLock - :* Just feat.config.applockInactivityTimeoutSecs + :* Just feat.config.enforce + :* Just feat.config.timeout :* Nil instance MakeFeature ClassifiedDomainsConfig where @@ -336,9 +336,12 @@ instance MakeFeature MlsE2EIdConfig where defCfg { verificationExpiration = maybe defCfg.verificationExpiration fromIntegral gracePeriod, - acmeDiscoveryUrl = acmeDiscoveryUrl <|> defCfg.acmeDiscoveryUrl, - crlProxy = crlProxy <|> defCfg.crlProxy, - useProxyOnMobile = fromMaybe defCfg.useProxyOnMobile useProxyOnMobile + acmeDiscoveryUrl = Alt acmeDiscoveryUrl <|> defCfg.acmeDiscoveryUrl, + crlProxy = Alt crlProxy <|> defCfg.crlProxy, + useProxyOnMobile = + fromMaybe + defCfg.useProxyOnMobile + (fmap UseProxyOnMobile useProxyOnMobile) } ) @@ -346,9 +349,9 @@ instance MakeFeature MlsE2EIdConfig where Just feat.lockStatus :* Just feat.status :* Just (truncate feat.config.verificationExpiration) - :* feat.config.acmeDiscoveryUrl - :* feat.config.crlProxy - :* Just feat.config.useProxyOnMobile + :* getAlt feat.config.acmeDiscoveryUrl + :* getAlt feat.config.crlProxy + :* Just (unUseProxyOnMobile feat.config.useProxyOnMobile) :* Nil -- Optional time stamp. A 'Nothing' value is represented as 0. diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index cc49154eddb..40de3bd566d 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -77,7 +77,7 @@ import Wire.API.Internal.Notification hiding (target) import Wire.API.Routes.Internal.Galley.TeamsIntra as TeamsIntra import Wire.API.Routes.Version import Wire.API.Team -import Wire.API.Team.Feature +import Wire.API.Team.Feature hiding (timeout) import Wire.API.Team.Member import Wire.API.Team.Member qualified as Member import Wire.API.Team.Member qualified as Teams diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index eee7f27c1ad..6788715ef86 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -54,7 +54,7 @@ import Test.Tasty.Runners import TestSetup import Wire.API.Internal.Notification (ntfPayload) import Wire.API.Provider.Service -import Wire.API.Team.Feature +import Wire.API.Team.Feature hiding (timeout) import Wire.API.Team.Feature qualified as Public import Wire.API.Team.LegalHold import Wire.API.Team.LegalHold.External From c5c2570953f647df7772f2ceef9f2190ea40d40c Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 18 Feb 2025 16:53:08 +0000 Subject: [PATCH 02/25] add DB flag for migration state --- cassandra-schema.cql | 1 + services/galley/galley.cabal | 1 + services/galley/src/Galley/Schema/Run.hs | 4 ++- .../V95_TeamFeatureDataMigrationState.hs | 33 +++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 58f4da9207c..075bf3128c6 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -1207,6 +1207,7 @@ CREATE TABLE galley_test.team_features ( guest_links_status int, legalhold_status int, limited_event_fanout_status int, + migration_state int, mls_allowed_ciphersuites set, mls_default_ciphersuite int, mls_default_protocol int, diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index c04af28f356..ba5bc8a86c7 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -280,6 +280,7 @@ library Galley.Schema.V92_MlsE2EIdConfig Galley.Schema.V93_ConferenceCallingSftForOneToOne Galley.Schema.V94_DomainRegistrationConfig + Galley.Schema.V95_TeamFeatureDataMigrationState Galley.Types.Clients Galley.Types.ToUserRole Galley.Types.UserList diff --git a/services/galley/src/Galley/Schema/Run.hs b/services/galley/src/Galley/Schema/Run.hs index 0019e0f7de1..a7255aebeec 100644 --- a/services/galley/src/Galley/Schema/Run.hs +++ b/services/galley/src/Galley/Schema/Run.hs @@ -95,6 +95,7 @@ import Galley.Schema.V91_TeamMemberDeletedLimitedEventFanout qualified as V91_Te import Galley.Schema.V92_MlsE2EIdConfig qualified as V92_MlsE2EIdConfig import Galley.Schema.V93_ConferenceCallingSftForOneToOne qualified as V93_ConferenceCallingSftForOneToOne import Galley.Schema.V94_DomainRegistrationConfig qualified as V94_DomainRegistrationConfig +import Galley.Schema.V95_TeamFeatureDataMigrationState qualified as V95_TeamFeatureDataMigrationState import Imports import Options.Applicative import System.Logger.Extended qualified as Log @@ -190,7 +191,8 @@ migrations = V91_TeamMemberDeletedLimitedEventFanout.migration, V92_MlsE2EIdConfig.migration, V93_ConferenceCallingSftForOneToOne.migration, - V94_DomainRegistrationConfig.migration + V94_DomainRegistrationConfig.migration, + V95_TeamFeatureDataMigrationState.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/V95_TeamFeatureDataMigrationState.hs b/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs new file mode 100644 index 00000000000..da186197ba5 --- /dev/null +++ b/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs @@ -0,0 +1,33 @@ +-- 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 . +module Galley.Schema.V95_TeamFeatureDataMigrationState + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 95 "temporary column to track the state of the team feature migration" $ + schema' + [r| ALTER TABLE team_features ADD ( + migration_state int + ) + |] From a39797971c6a5d1b17be70dcc4bf81861849019d Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 18 Feb 2025 17:41:09 +0000 Subject: [PATCH 03/25] honor migration state in team feature store --- libs/wire-api/src/Wire/API/Team/Feature.hs | 22 ++++++++++++++ .../src/Galley/Cassandra/TeamFeatures.hs | 30 +++++++++++++++++-- .../src/Galley/Effects/TeamFeatureStore.hs | 1 + 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 829485fb210..ae482584622 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -89,6 +89,7 @@ module Wire.API.Team.Feature AllTeamFeatures, parseDbFeature, mkAllFeatures, + TeamFeatureMigrationState (..), ) where @@ -1531,3 +1532,24 @@ mkAllFeatures m = where get :: forall cfg. (IsFeatureConfig cfg) => K (Maybe a) cfg get = K $ M.lookup (featureName @cfg) m + +-------------------------------------------------------------------------------- +-- Team Feature Migration + +data TeamFeatureMigrationState = MigrationNotStarted | MigrationInProgress | MigrationCompleted + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform TeamFeatureMigrationState) + +instance Cass.Cql TeamFeatureMigrationState where + ctype = Cass.Tagged Cass.IntColumn + + fromCql (Cass.CqlInt n) = case n of + 0 -> pure MigrationNotStarted + 1 -> pure MigrationInProgress + 2 -> pure MigrationCompleted + _ -> Left "fromCql: Invalid TeamFeatureMigrationState value" + fromCql _ = Left "fromCql: TeamFeatureMigrationState: CqlInt or CqlNull expected" + + toCql MigrationNotStarted = Cass.CqlInt 0 + toCql MigrationInProgress = Cass.CqlInt 1 + toCql MigrationCompleted = Cass.CqlInt 2 diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 04a5ab8f3ab..9b985c52dc6 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -59,12 +59,36 @@ interpretTeamFeatureStoreToCassandra = interpret $ \case TFS.GetAllDbFeatures tid -> do logEffect "TeamFeatureStore.GetAllTeamFeatures" embedClient $ getAllDbFeatures tid + TFS.GetMigrationState tid -> do + embedClient $ getMigrationState tid + +getMigrationState :: (MonadClient m) => TeamId -> m TeamFeatureMigrationState +getMigrationState tid = do + maybe MigrationNotStarted runIdentity <$> retry x1 (query1 cql (params LocalQuorum (Identity tid))) + where + cql :: PrepQuery R (Identity TeamId) (Identity TeamFeatureMigrationState) + cql = "SELECT migration_state FROM team_features WHERE team_id = ?" getDbFeature :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (DbFeature cfg) -getDbFeature = $(featureCases [|fetchFeature|]) +getDbFeature cfg tid = do + migrationState <- getMigrationState tid + case migrationState of + MigrationNotStarted -> $(featureCases [|fetchFeature|]) cfg tid + MigrationInProgress -> $(featureCases [|fetchFeature|]) cfg tid + MigrationCompleted -> todo setDbFeature :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> LockableFeature cfg -> m () -setDbFeature = $(featureCases [|storeFeature|]) +setDbFeature feature tid cfg = do + migrationState <- getMigrationState tid + case migrationState of + MigrationNotStarted -> $(featureCases [|storeFeature|]) feature tid cfg + MigrationInProgress -> todo + MigrationCompleted -> todo setFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> Tagged cfg LockStatus -> m () -setFeatureLockStatus = $(featureCases [|storeFeatureLockStatus|]) +setFeatureLockStatus feature tid ls = do + migrationState <- getMigrationState tid + case migrationState of + MigrationNotStarted -> $(featureCases [|storeFeatureLockStatus|]) feature tid ls + MigrationInProgress -> todo + MigrationCompleted -> todo diff --git a/services/galley/src/Galley/Effects/TeamFeatureStore.hs b/services/galley/src/Galley/Effects/TeamFeatureStore.hs index b756ff9281f..8677f7ccb95 100644 --- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs +++ b/services/galley/src/Galley/Effects/TeamFeatureStore.hs @@ -40,6 +40,7 @@ data TeamFeatureStore m a where GetAllDbFeatures :: TeamId -> TeamFeatureStore m (AllFeatures DbFeature) + GetMigrationState :: TeamId -> TeamFeatureStore m TeamFeatureMigrationState getDbFeature :: (Member TeamFeatureStore r, IsFeatureConfig cfg) => From 5dcfcfa556a1bf951cd71922f849b9f62b74ea4d Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 19 Feb 2025 09:54:52 +0100 Subject: [PATCH 04/25] Implement dynamic feature store operations --- libs/wire-api/src/Wire/API/Team/Feature.hs | 43 ++-- libs/wire-api/src/Wire/API/Team/Feature/TH.hs | 10 + libs/wire-api/wire-api.cabal | 2 + services/galley/galley.cabal | 1 + .../Galley/Cassandra/GetAllTeamFeatures.hs | 12 +- .../src/Galley/Cassandra/TeamFeatures.hs | 183 ++++++++++++++++-- 6 files changed, 211 insertions(+), 40 deletions(-) create mode 100644 libs/wire-api/src/Wire/API/Team/Feature/TH.hs diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index ae482584622..b68beca86cb 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -88,6 +88,7 @@ module Wire.API.Team.Feature npUpdate, AllTeamFeatures, parseDbFeature, + serialiseDbFeature, mkAllFeatures, TeamFeatureMigrationState (..), ) @@ -204,7 +205,8 @@ class ToSchema cfg, Default (LockableFeature cfg), KnownSymbol (FeatureSymbol cfg), - NpProject cfg Features + NpProject cfg Features, + ParseDbFeature cfg ) => IsFeatureConfig cfg where @@ -374,6 +376,9 @@ data LockableFeaturePatch (cfg :: Type) = LockableFeaturePatch deriving stock (Eq, Show) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (LockableFeaturePatch cfg)) +instance Default (LockableFeaturePatch cfg) where + def = LockableFeaturePatch Nothing Nothing Nothing + -- | The ToJSON implementation of `LockableFeaturePatch` will encode the trivial config as `"config": {}` -- when the value is a `Just`, if it's `Nothing` it will be omitted, which is the important part. instance (ToSchema cfg) => ToSchema (LockableFeaturePatch cfg) where @@ -1508,30 +1513,38 @@ instance (BareB b, ToSchema (b Covered Identity)) => ToSchema (BarbieFeature b) parseDbFeature :: forall cfg. (ParseDbFeature cfg) => - Maybe FeatureStatus -> - Maybe LockStatus -> - Maybe DbConfig -> + LockableFeaturePatch DbConfig -> A.Parser (DbFeature cfg) -parseDbFeature status lockStatus dbConfig = +parseDbFeature feat = do - f <- maybe (pure id) parseDbConfig dbConfig + f <- maybe (pure id) parseDbConfig feat.config pure $ - foldMap dbFeatureStatus status - <> foldMap dbFeatureLockStatus lockStatus + foldMap dbFeatureStatus feat.status + <> foldMap dbFeatureLockStatus feat.lockStatus <> dbFeatureModConfig f +serialiseDbFeature :: (IsFeatureConfig cfg) => LockableFeature cfg -> LockableFeaturePatch DbConfig +serialiseDbFeature feat = + LockableFeaturePatch + { status = Just feat.status, + lockStatus = Just feat.lockStatus, + config = Just . DbConfig . schemaToJSON $ feat.config + } + -- | Convert a map indexed by feature name to an NP value. mkAllFeatures :: - forall cfgs a. - (Default a, All IsFeatureConfig cfgs) => - Map Text a -> - NP (K a) cfgs + forall cfgs. + ( All IsFeatureConfig cfgs, + All ParseDbFeature cfgs + ) => + Map Text (LockableFeaturePatch DbConfig) -> + A.Parser (NP DbFeature cfgs) mkAllFeatures m = - hmap (mapKK (fromMaybe def)) $ + hctraverse' (Proxy @ParseDbFeature) (parseDbFeature . unK) $ hcpure (Proxy @IsFeatureConfig) get where - get :: forall cfg. (IsFeatureConfig cfg) => K (Maybe a) cfg - get = K $ M.lookup (featureName @cfg) m + get :: forall cfg. (IsFeatureConfig cfg) => K (LockableFeaturePatch DbConfig) cfg + get = K $ M.findWithDefault def (featureName @cfg) m -------------------------------------------------------------------------------- -- Team Feature Migration diff --git a/libs/wire-api/src/Wire/API/Team/Feature/TH.hs b/libs/wire-api/src/Wire/API/Team/Feature/TH.hs new file mode 100644 index 00000000000..bea09ca7802 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Team/Feature/TH.hs @@ -0,0 +1,10 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.API.Team.Feature.TH where + +import Data.Constraint +import Data.Singletons.Base.TH +import Wire.API.Team.Feature + +featureSingIsFeature :: forall cfg. FeatureSingleton cfg -> Dict (IsFeatureConfig cfg) +featureSingIsFeature s = $(cases ''FeatureSingleton [|s|] [|Dict|]) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index e269aba60ae..9af7d635c42 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -218,6 +218,7 @@ library Wire.API.Team.Conversation Wire.API.Team.Export Wire.API.Team.Feature + Wire.API.Team.Feature.TH Wire.API.Team.HardTruncationLimit Wire.API.Team.Invitation Wire.API.Team.LegalHold @@ -275,6 +276,7 @@ library , cereal , comonad , conduit + , constraints , containers >=0.5 , cookie , crypton diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index ba5bc8a86c7..558ce09a4e8 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -306,6 +306,7 @@ library , cassandra-util >=0.16.2 , cassava >=0.5.2 , comonad + , constraints , containers >=0.5 , crypton , crypton-x509 diff --git a/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs b/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs index 3019ce00275..3b378b65816 100644 --- a/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs @@ -1,6 +1,6 @@ {-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-} -module Galley.Cassandra.GetAllTeamFeatures (getAllDbFeatures) where +module Galley.Cassandra.GetAllTeamFeatures (getAllDbFeaturesLegacy) where import Cassandra import Data.Id @@ -71,16 +71,16 @@ instance where concatColumns = featureColumns @cfg `appendNP` concatColumns @cfgs -getAllDbFeatures :: +getAllDbFeaturesLegacy :: forall row mrow m. - ( MonadClient m, - row ~ AllFeatureRow, + ( row ~ AllFeatureRow, Tuple (TupleP mrow), IsProductType (TupleP mrow) mrow, - AllZip (IsF Maybe) row mrow + AllZip (IsF Maybe) row mrow, + MonadClient m ) => TeamId -> m (AllFeatures DbFeature) -getAllDbFeatures tid = do +getAllDbFeaturesLegacy tid = do mRow <- fetchFeatureRow @row @mrow tid (concatColumns @Features) pure . rowToAllFeatures $ fromMaybe emptyRow mRow diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 9b985c52dc6..474a61f13f6 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. @@ -24,7 +25,12 @@ module Galley.Cassandra.TeamFeatures where import Cassandra +import Data.Aeson.Types qualified as A +import Data.Constraint import Data.Id +import Data.Map qualified as M +import Data.Text.Lazy qualified as LT +import Galley.API.Error import Galley.API.Teams.Features.Get import Galley.Cassandra.FeatureTH import Galley.Cassandra.GetAllTeamFeatures @@ -35,13 +41,16 @@ import Galley.Cassandra.Util import Galley.Effects.TeamFeatureStore qualified as TFS import Imports import Polysemy +import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog import Wire.API.Team.Feature +import Wire.API.Team.Feature.TH interpretTeamFeatureStoreToCassandra :: ( Member (Embed IO) r, Member (Input ClientState) r, + Member (Error InternalError) r, Member TinyLog r ) => Sem (TFS.TeamFeatureStore ': r) a -> @@ -49,46 +58,182 @@ interpretTeamFeatureStoreToCassandra :: interpretTeamFeatureStoreToCassandra = interpret $ \case TFS.GetDbFeature sing tid -> do logEffect "TeamFeatureStore.GetFeatureConfig" - embedClient $ getDbFeature sing tid + getDbFeature sing tid TFS.SetDbFeature sing tid feat -> do logEffect "TeamFeatureStore.SetFeatureConfig" - embedClient $ setDbFeature sing tid feat + setDbFeature sing tid feat TFS.SetFeatureLockStatus sing tid lock -> do logEffect "TeamFeatureStore.SetFeatureLockStatus" - embedClient $ setFeatureLockStatus sing tid (Tagged lock) + setFeatureLockStatus sing tid (Tagged lock) TFS.GetAllDbFeatures tid -> do logEffect "TeamFeatureStore.GetAllTeamFeatures" - embedClient $ getAllDbFeatures tid + getAllDbFeatures tid TFS.GetMigrationState tid -> do - embedClient $ getMigrationState tid + getMigrationState tid -getMigrationState :: (MonadClient m) => TeamId -> m TeamFeatureMigrationState -getMigrationState tid = do +getMigrationState :: + ( Member (Input ClientState) r, + Member (Embed IO) r + ) => + TeamId -> + Sem r TeamFeatureMigrationState +getMigrationState tid = embedClient $ do maybe MigrationNotStarted runIdentity <$> retry x1 (query1 cql (params LocalQuorum (Identity tid))) where cql :: PrepQuery R (Identity TeamId) (Identity TeamFeatureMigrationState) cql = "SELECT migration_state FROM team_features WHERE team_id = ?" -getDbFeature :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (DbFeature cfg) +getDbFeature :: + ( Member (Input ClientState) r, + Member (Embed IO) r, + Member (Error InternalError) r + ) => + FeatureSingleton cfg -> + TeamId -> + Sem r (DbFeature cfg) getDbFeature cfg tid = do migrationState <- getMigrationState tid case migrationState of - MigrationNotStarted -> $(featureCases [|fetchFeature|]) cfg tid - MigrationInProgress -> $(featureCases [|fetchFeature|]) cfg tid - MigrationCompleted -> todo + MigrationCompleted -> getDbFeatureDyn cfg tid + _ -> embedClient $ $(featureCases [|fetchFeature|]) cfg tid -setDbFeature :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> LockableFeature cfg -> m () +setDbFeature :: + ( Member (Input ClientState) r, + Member (Error InternalError) r, + Member (Embed IO) r + ) => + FeatureSingleton cfg -> + TeamId -> + LockableFeature cfg -> + Sem r () setDbFeature feature tid cfg = do migrationState <- getMigrationState tid case migrationState of - MigrationNotStarted -> $(featureCases [|storeFeature|]) feature tid cfg - MigrationInProgress -> todo - MigrationCompleted -> todo + MigrationNotStarted -> embedClient $ $(featureCases [|storeFeature|]) feature tid cfg + MigrationInProgress -> readOnlyError + MigrationCompleted -> setDbFeatureDyn feature tid cfg -setFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> Tagged cfg LockStatus -> m () +setFeatureLockStatus :: + ( Member (Input ClientState) r, + Member (Error InternalError) r, + Member (Embed IO) r + ) => + FeatureSingleton cfg -> + TeamId -> + Tagged cfg LockStatus -> + Sem r () setFeatureLockStatus feature tid ls = do migrationState <- getMigrationState tid case migrationState of - MigrationNotStarted -> $(featureCases [|storeFeatureLockStatus|]) feature tid ls - MigrationInProgress -> todo - MigrationCompleted -> todo + MigrationNotStarted -> embedClient $ $(featureCases [|storeFeatureLockStatus|]) feature tid ls + MigrationInProgress -> readOnlyError + MigrationCompleted -> setFeatureLockStatusDyn feature tid ls + +getAllDbFeatures :: + ( Member (Input ClientState) r, + Member (Error InternalError) r, + Member (Embed IO) r + ) => + TeamId -> + Sem r (AllFeatures DbFeature) +getAllDbFeatures tid = do + migrationState <- getMigrationState tid + case migrationState of + MigrationCompleted -> getAllDbFeaturesDyn tid + _ -> embedClient $ getAllDbFeaturesLegacy tid + +readOnlyError :: (Member (Error InternalError) r) => Sem r a +readOnlyError = throw (InternalErrorWithDescription "migration in progress") + +-------------------------------------------------------------------------------- +-- Dynamic features + +getDbFeatureDyn :: + forall cfg r. + ( Member (Input ClientState) r, + Member (Embed IO) r, + Member (Error InternalError) r + ) => + FeatureSingleton cfg -> + TeamId -> + Sem r (DbFeature cfg) +getDbFeatureDyn sing tid = case featureSingIsFeature sing of + Dict -> do + let q :: PrepQuery R (TeamId, Text) (Maybe FeatureStatus, Maybe LockStatus, Maybe DbConfig) + q = "select status, lock_status, config from team_features_dyn where team = ? and feature = ?" + (embedClient $ retry x1 $ query1 q (params LocalQuorum (tid, featureName @cfg))) >>= \case + Nothing -> pure mempty + Just (status, lockStatus, config) -> + runFeatureParser . parseDbFeature $ + LockableFeaturePatch {..} + +setDbFeatureDyn :: + forall cfg r. + ( Member (Input ClientState) r, + Member (Embed IO) r + ) => + FeatureSingleton cfg -> + TeamId -> + LockableFeature cfg -> + Sem r () +setDbFeatureDyn sing tid feat = case featureSingIsFeature sing of + Dict -> do + let q :: PrepQuery W (Maybe FeatureStatus, Maybe LockStatus, Maybe DbConfig, TeamId, Text) () + q = "update team_features_dyn set status = ?, lock_status = ?, config = ? where team = ? and feature = ?" + dbFeat = serialiseDbFeature feat + embedClient $ + retry x5 $ + write + q + ( params + LocalQuorum + ( dbFeat.status, + dbFeat.lockStatus, + dbFeat.config, + tid, + featureName @cfg + ) + ) + +setFeatureLockStatusDyn :: + forall cfg r. + ( Member (Input ClientState) r, + Member (Embed IO) r + ) => + FeatureSingleton cfg -> + TeamId -> + Tagged cfg LockStatus -> + Sem r () +setFeatureLockStatusDyn sing tid (Tagged lockStatus) = case featureSingIsFeature sing of + Dict -> do + let q :: PrepQuery W (LockStatus, TeamId, Text) () + q = "update team_features_dyn set lock_status = ? where team = ? and feature = ?" + embedClient $ + retry x5 $ + write q (params LocalQuorum (lockStatus, tid, featureName @cfg)) + +getAllDbFeaturesDyn :: + ( Member (Embed IO) r, + Member (Error InternalError) r, + Member (Input ClientState) r + ) => + TeamId -> + Sem r (AllFeatures DbFeature) +getAllDbFeaturesDyn tid = do + let q :: PrepQuery R (Identity TeamId) (Text, Maybe FeatureStatus, Maybe LockStatus, Maybe DbConfig) + q = "select feature, status, lock_status, config from team_features_dyn where team = ?" + rows <- embedClient $ retry x1 $ query q (params LocalQuorum (Identity tid)) + let m = M.fromList $ do + (name, status, lockStatus, config) <- rows + pure (name, LockableFeaturePatch {..}) + runFeatureParser $ mkAllFeatures m + +runFeatureParser :: + forall r a. + (Member (Error InternalError) r) => + A.Parser a -> + Sem r a +runFeatureParser p = + mapError (InternalErrorWithDescription . LT.pack) + . fromEither + $ A.parseEither (const p) () From 347af669fc5adfa2a008f8e2096cca113509a188 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 19 Feb 2025 10:41:06 +0100 Subject: [PATCH 05/25] Fix migration state query --- libs/wire-api/src/Wire/API/Team/Feature.hs | 3 +++ services/galley/src/Galley/Cassandra/TeamFeatures.hs | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index b68beca86cb..33276e7d5ac 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1553,6 +1553,9 @@ data TeamFeatureMigrationState = MigrationNotStarted | MigrationInProgress | Mig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform TeamFeatureMigrationState) +instance Default TeamFeatureMigrationState where + def = MigrationNotStarted + instance Cass.Cql TeamFeatureMigrationState where ctype = Cass.Tagged Cass.IntColumn diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 474a61f13f6..1b53eab0c47 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -27,6 +27,7 @@ where import Cassandra import Data.Aeson.Types qualified as A import Data.Constraint +import Data.Default import Data.Id import Data.Map qualified as M import Data.Text.Lazy qualified as LT @@ -78,9 +79,9 @@ getMigrationState :: TeamId -> Sem r TeamFeatureMigrationState getMigrationState tid = embedClient $ do - maybe MigrationNotStarted runIdentity <$> retry x1 (query1 cql (params LocalQuorum (Identity tid))) + fromMaybe def . join . fmap runIdentity <$> retry x1 (query1 cql (params LocalQuorum (Identity tid))) where - cql :: PrepQuery R (Identity TeamId) (Identity TeamFeatureMigrationState) + cql :: PrepQuery R (Identity TeamId) (Identity (Maybe TeamFeatureMigrationState)) cql = "SELECT migration_state FROM team_features WHERE team_id = ?" getDbFeature :: From a834400c061dce062d1e44fcb06db7a2b0f0a6e4 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 19 Feb 2025 10:41:15 +0100 Subject: [PATCH 06/25] Add dynamic feature table --- .../Schema/V95_TeamFeatureDataMigrationState.hs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs b/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs index da186197ba5..acff1f80213 100644 --- a/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs +++ b/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs @@ -25,9 +25,20 @@ import Text.RawString.QQ migration :: Migration migration = - Migration 95 "temporary column to track the state of the team feature migration" $ + Migration 95 "temporary column to track the state of the team feature migration, new feature table" $ do schema' [r| ALTER TABLE team_features ADD ( migration_state int ) |] + + schema' + [r| CREATE TABLE team_features_dyn ( + team uuid, + feature ascii, + status int, + lock_status int, + config text, + PRIMARY KEY (team, feature) + ) WITH compaction = {'class': 'LeveledCompactionStrategy'}; + |] From 6dbc23a358395c66500acbd7560cffc39cf4dd26 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 19 Feb 2025 11:08:23 +0100 Subject: [PATCH 07/25] Add internal endpoint for setting migration state --- .../src/Wire/API/Routes/Internal/Galley.hs | 10 ++++++++++ libs/wire-api/src/Wire/API/Team/Feature.hs | 10 ++++++++++ services/galley/src/Galley/API/Internal.hs | 3 +++ .../src/Galley/Cassandra/TeamFeatures.hs | 19 +++++++++++++++++-- .../src/Galley/Effects/TeamFeatureStore.hs | 5 ++++- 5 files changed, 44 insertions(+), 3 deletions(-) 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 3daa6448544..fbff39b629b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -99,6 +99,16 @@ type IFeatureAPI = :> Get '[JSON] AllTeamFeatures ) :<|> IFeatureStatusLockStatusPut DomainRegistrationConfig + -- migration state + :<|> Named + "put-feature-migration-state" + ( Summary "Manually set migration state (for testing)" + :> "teams" + :> Capture "team" TeamId + :> "feature-migration-state" + :> ReqBody '[JSON] TeamFeatureMigrationState + :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "OK") + ) type InternalAPI = "i" :> InternalAPIBase diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 33276e7d5ac..5b4fbc94985 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1552,10 +1552,20 @@ mkAllFeatures m = data TeamFeatureMigrationState = MigrationNotStarted | MigrationInProgress | MigrationCompleted deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform TeamFeatureMigrationState) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema TeamFeatureMigrationState) instance Default TeamFeatureMigrationState where def = MigrationNotStarted +instance ToSchema TeamFeatureMigrationState where + schema = + enum @Text "TeamFeatureMigrationState" $ + mconcat + [ element "not_started" MigrationNotStarted, + element "in_progress" MigrationInProgress, + element "completed" MigrationCompleted + ] + instance Cass.Cql TeamFeatureMigrationState where ctype = Cass.Tagged Cass.IntColumn diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 27433530eec..e634b1de2c5 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -61,6 +61,7 @@ import Galley.Effects.CustomBackendStore import Galley.Effects.LegalHoldStore as LegalHoldStore import Galley.Effects.MemberStore qualified as E import Galley.Effects.ServiceStore +import Galley.Effects.TeamFeatureStore qualified as E import Galley.Effects.TeamStore import Galley.Effects.TeamStore qualified as E import Galley.Monad @@ -288,6 +289,8 @@ featureAPI = -- all features <@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser) <@> mkNamedAPI @'("ilock", DomainRegistrationConfig) (updateLockStatus @DomainRegistrationConfig) + -- migration state + <@> mkNamedAPI @"put-feature-migration-state" E.setMigrationState rmUser :: forall p1 p2 r. diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 1b53eab0c47..d72500ec2c2 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -69,8 +69,23 @@ interpretTeamFeatureStoreToCassandra = interpret $ \case TFS.GetAllDbFeatures tid -> do logEffect "TeamFeatureStore.GetAllTeamFeatures" getAllDbFeatures tid - TFS.GetMigrationState tid -> do - getMigrationState tid + TFS.SetMigrationState tid state -> do + logEffect "TeamFeatureStore.SetMigrationState" + setMigrationState tid state + +setMigrationState :: + ( Member (Input ClientState) r, + Member (Embed IO) r + ) => + TeamId -> + TeamFeatureMigrationState -> + Sem r () +setMigrationState tid state = embedClient $ do + retry x5 $ + write cql (params LocalQuorum (state, tid)) + where + cql :: PrepQuery W (TeamFeatureMigrationState, TeamId) () + cql = "UPDATE team_features SET migration_state = ? WHERE team_id = ?" getMigrationState :: ( Member (Input ClientState) r, diff --git a/services/galley/src/Galley/Effects/TeamFeatureStore.hs b/services/galley/src/Galley/Effects/TeamFeatureStore.hs index 8677f7ccb95..82549dcdaaa 100644 --- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs +++ b/services/galley/src/Galley/Effects/TeamFeatureStore.hs @@ -40,7 +40,7 @@ data TeamFeatureStore m a where GetAllDbFeatures :: TeamId -> TeamFeatureStore m (AllFeatures DbFeature) - GetMigrationState :: TeamId -> TeamFeatureStore m TeamFeatureMigrationState + SetMigrationState :: TeamId -> TeamFeatureMigrationState -> TeamFeatureStore m () getDbFeature :: (Member TeamFeatureStore r, IsFeatureConfig cfg) => @@ -66,3 +66,6 @@ setFeatureLockStatus tid lockStatus = getAllDbFeatures :: (Member TeamFeatureStore r) => TeamId -> Sem r (AllFeatures DbFeature) getAllDbFeatures tid = send (GetAllDbFeatures tid) + +setMigrationState :: (Member TeamFeatureStore r) => TeamId -> TeamFeatureMigrationState -> Sem r () +setMigrationState tid state = send (SetMigrationState tid state) From e9f05cb1a388590de65d0d54f510086edf89a310 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 19 Feb 2025 11:48:11 +0000 Subject: [PATCH 08/25] changed schema slightly, integration test all features --- cassandra-schema.cql | 23 +++++++++++++++++++ integration/test/API/GalleyInternal.hs | 7 ++++++ integration/test/Test/FeatureFlags.hs | 5 ++-- integration/test/Test/FeatureFlags/Util.hs | 9 ++++++++ .../src/Galley/Cassandra/TeamFeatures.hs | 4 ++-- .../V95_TeamFeatureDataMigrationState.hs | 2 +- 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 075bf3128c6..9af5e004580 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -1517,6 +1517,29 @@ CREATE TABLE galley_test.legalhold_service ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; +CREATE TABLE galley_test.team_features_dyn ( + team uuid, + feature text, + config text, + lock_status int, + status int, + PRIMARY KEY (team, feature) +) WITH CLUSTERING ORDER BY (feature ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + 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 max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + CREATE TABLE galley_test.conversation_codes ( key ascii, scope int, diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index be0d09fbecf..4fee51bf960 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -1,7 +1,9 @@ module API.GalleyInternal where +import qualified Data.Aeson as A import qualified Data.Aeson as Aeson import Data.String.Conversions (cs) +import qualified Data.Text as T import qualified Data.Vector as Vector import GHC.Stack import Testlib.Prelude @@ -136,3 +138,8 @@ getTeam :: (HasCallStack, MakesValue domain) => domain -> String -> App Response getTeam domain tid = do req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid] submit "GET" $ req + +setTeamFeatureMigrationState :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +setTeamFeatureMigrationState domain tid state = do + req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "feature-migration-state"] + submit "PUT" $ req & addJSON (A.String (T.pack state)) diff --git a/integration/test/Test/FeatureFlags.hs b/integration/test/Test/FeatureFlags.hs index e1ecdae4da2..829aeca4a19 100644 --- a/integration/test/Test/FeatureFlags.hs +++ b/integration/test/Test/FeatureFlags.hs @@ -44,9 +44,10 @@ testLimitedEventFanout = do -- | Call 'GET /teams/:tid/features' and 'GET /feature-configs', and check if all -- features are there. -testAllFeatures :: (HasCallStack) => App () -testAllFeatures = do +testAllFeatures :: (HasCallStack) => FeatureTable -> App () +testAllFeatures ft = do (_, tid, m : _) <- createTeam OwnDomain 2 + updateMigrationState OwnDomain tid ft bindResponse (Public.getTeamFeatures m tid) $ \resp -> do resp.status `shouldMatchInt` 200 defAllFeatures `shouldMatch` resp.json diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 821b9220146..402a97be60d 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -345,3 +345,12 @@ runFeatureTests domain access ft = do setField "ttl" "unlimited" =<< setField "lockStatus" "unlocked" update checkFeature ft.name owner tid expected + +data FeatureTable = FeatureTableLegacy | FeatureTableDyn + deriving (Show, Eq, Generic) + +updateMigrationState :: (HasCallStack, MakesValue domain) => domain -> String -> FeatureTable -> App () +updateMigrationState domain tid ft = case ft of + FeatureTableLegacy -> pure () + FeatureTableDyn -> do + Internal.setTeamFeatureMigrationState domain tid "completed" >>= assertSuccess diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index d72500ec2c2..4a4608bcab8 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -94,7 +94,7 @@ getMigrationState :: TeamId -> Sem r TeamFeatureMigrationState getMigrationState tid = embedClient $ do - fromMaybe def . join . fmap runIdentity <$> retry x1 (query1 cql (params LocalQuorum (Identity tid))) + fromMaybe def . (runIdentity =<<) <$> retry x1 (query1 cql (params LocalQuorum (Identity tid))) where cql :: PrepQuery R (Identity TeamId) (Identity (Maybe TeamFeatureMigrationState)) cql = "SELECT migration_state FROM team_features WHERE team_id = ?" @@ -177,7 +177,7 @@ getDbFeatureDyn sing tid = case featureSingIsFeature sing of Dict -> do let q :: PrepQuery R (TeamId, Text) (Maybe FeatureStatus, Maybe LockStatus, Maybe DbConfig) q = "select status, lock_status, config from team_features_dyn where team = ? and feature = ?" - (embedClient $ retry x1 $ query1 q (params LocalQuorum (tid, featureName @cfg))) >>= \case + embedClient (retry x1 $ query1 q (params LocalQuorum (tid, featureName @cfg))) >>= \case Nothing -> pure mempty Just (status, lockStatus, config) -> runFeatureParser . parseDbFeature $ diff --git a/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs b/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs index acff1f80213..2cf7b2fbd2b 100644 --- a/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs +++ b/services/galley/src/Galley/Schema/V95_TeamFeatureDataMigrationState.hs @@ -35,7 +35,7 @@ migration = schema' [r| CREATE TABLE team_features_dyn ( team uuid, - feature ascii, + feature text, status int, lock_status int, config text, From d36e94ab30e5f3f893cf73902b8746f0a5007c50 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 19 Feb 2025 11:52:33 +0000 Subject: [PATCH 09/25] Test new table in feature flags tests --- integration/test/Test/FeatureFlags.hs | 16 +++++----- integration/test/Test/FeatureFlags/AppLock.hs | 14 ++++----- .../Test/FeatureFlags/DigitalSignatures.hs | 9 +++--- .../Test/FeatureFlags/DomainRegistration.hs | 9 +++--- .../EnforceFileDownloadLocation.hs | 20 +++++++------ .../test/Test/FeatureFlags/FileSharing.hs | 9 +++--- .../test/Test/FeatureFlags/GuestLinks.hs | 9 +++--- .../test/Test/FeatureFlags/Initialisation.hs | 6 ++-- .../test/Test/FeatureFlags/LegalHold.hs | 21 +++++++++----- integration/test/Test/FeatureFlags/Mls.hs | 19 ++++++------ .../test/Test/FeatureFlags/MlsE2EId.hs | 29 ++++++++++--------- .../test/Test/FeatureFlags/MlsMigration.hs | 10 ++++--- .../FeatureFlags/OutlookCalIntegration.hs | 9 +++--- integration/test/Test/FeatureFlags/SSO.hs | 10 ++++--- .../FeatureFlags/SearchVisibilityAvailable.hs | 14 +++++---- .../FeatureFlags/SearchVisibilityInbound.hs | 5 ++-- .../Test/FeatureFlags/SelfDeletingMessages.hs | 19 ++++++------ .../SndFactorPasswordChallenge.hs | 11 +++---- integration/test/Test/FeatureFlags/Util.hs | 21 ++++++++++++-- .../Test/FeatureFlags/ValidateSAMLEmails.hs | 11 +++---- 20 files changed, 158 insertions(+), 113 deletions(-) diff --git a/integration/test/Test/FeatureFlags.hs b/integration/test/Test/FeatureFlags.hs index 829aeca4a19..0e2d9c59f0e 100644 --- a/integration/test/Test/FeatureFlags.hs +++ b/integration/test/Test/FeatureFlags.hs @@ -29,11 +29,11 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testLimitedEventFanout :: (HasCallStack) => App () -testLimitedEventFanout = do +testLimitedEventFanout :: (HasCallStack) => FeatureTable -> App () +testLimitedEventFanout ft = do let featureName = "limitedEventFanout" (_alice, team, _) <- createTeam OwnDomain 1 - -- getTeamFeatureStatus OwnDomain team "limitedEventFanout" "enabled" + updateMigrationState OwnDomain team ft bindResponse (Internal.getTeamFeature OwnDomain team featureName) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "disabled" @@ -71,9 +71,10 @@ testAllFeatures ft = do resp.status `shouldMatchInt` 200 defAllFeatures `shouldMatch` resp.json -testFeatureConfigConsistency :: (HasCallStack) => App () -testFeatureConfigConsistency = do +testFeatureConfigConsistency :: (HasCallStack) => FeatureTable -> App () +testFeatureConfigConsistency ft = do (_, tid, m : _) <- createTeam OwnDomain 2 + updateMigrationState OwnDomain tid ft allFeaturesRes <- Public.getFeatureConfigs m >>= parseObjectKeys @@ -89,9 +90,10 @@ testFeatureConfigConsistency = do (A.Object hm) -> pure (Set.fromList . map (show . A.toText) . KM.keys $ hm) x -> assertFailure ("JSON was not an object, but " <> show x) -testNonMemberAccess :: (HasCallStack) => Feature -> App () -testNonMemberAccess (Feature featureName) = do +testNonMemberAccess :: (HasCallStack) => FeatureTable -> Feature -> App () +testNonMemberAccess ft (Feature featureName) = do (_, tid, _) <- createTeam OwnDomain 0 + updateMigrationState OwnDomain tid ft nonMember <- randomUser OwnDomain def Public.getTeamFeature nonMember tid featureName >>= assertForbidden diff --git a/integration/test/Test/FeatureFlags/AppLock.hs b/integration/test/Test/FeatureFlags/AppLock.hs index f031403a98d..95600c2807b 100644 --- a/integration/test/Test/FeatureFlags/AppLock.hs +++ b/integration/test/Test/FeatureFlags/AppLock.hs @@ -4,15 +4,15 @@ import qualified Data.Aeson as A import Test.FeatureFlags.Util import Testlib.Prelude -testPatchAppLock :: (HasCallStack) => App () -testPatchAppLock = do - checkPatch OwnDomain "appLock" +testPatchAppLock :: (HasCallStack) => FeatureTable -> App () +testPatchAppLock table = do + checkPatchWithTable table OwnDomain "appLock" $ object ["lockStatus" .= "locked"] - checkPatch OwnDomain "appLock" + checkPatchWithTable table OwnDomain "appLock" $ object ["status" .= "disabled"] - checkPatch OwnDomain "appLock" + checkPatchWithTable table OwnDomain "appLock" $ object ["lockStatus" .= "locked", "status" .= "disabled"] - checkPatch OwnDomain "appLock" + checkPatchWithTable table OwnDomain "appLock" $ object [ "lockStatus" .= "unlocked", "config" @@ -21,7 +21,7 @@ testPatchAppLock = do "inactivityTimeoutSecs" .= A.Number 120 ] ] - checkPatch OwnDomain "appLock" + checkPatchWithTable table OwnDomain "appLock" $ object [ "config" .= object diff --git a/integration/test/Test/FeatureFlags/DigitalSignatures.hs b/integration/test/Test/FeatureFlags/DigitalSignatures.hs index 0a00bc33926..946b0e76cf0 100644 --- a/integration/test/Test/FeatureFlags/DigitalSignatures.hs +++ b/integration/test/Test/FeatureFlags/DigitalSignatures.hs @@ -4,12 +4,13 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchDigitalSignatures :: (HasCallStack) => App () -testPatchDigitalSignatures = checkPatch OwnDomain "digitalSignatures" enabled +testPatchDigitalSignatures :: (HasCallStack) => FeatureTable -> App () +testPatchDigitalSignatures table = checkPatchWithTable table OwnDomain "digitalSignatures" enabled -testDigitalSignaturesInternal :: (HasCallStack) => App () -testDigitalSignaturesInternal = do +testDigitalSignaturesInternal :: (HasCallStack) => FeatureTable -> App () +testDigitalSignaturesInternal table = do (alice, tid, _) <- createTeam OwnDomain 0 + updateMigrationState OwnDomain tid table withWebSocket alice $ \ws -> do setFlag InternalAPI ws tid "digitalSignatures" disabled setFlag InternalAPI ws tid "digitalSignatures" enabled diff --git a/integration/test/Test/FeatureFlags/DomainRegistration.hs b/integration/test/Test/FeatureFlags/DomainRegistration.hs index 8ff0d3a1404..dd342677ead 100644 --- a/integration/test/Test/FeatureFlags/DomainRegistration.hs +++ b/integration/test/Test/FeatureFlags/DomainRegistration.hs @@ -5,12 +5,13 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchDomainRegistration :: (HasCallStack) => App () -testPatchDomainRegistration = checkPatch OwnDomain "domainRegistration" enabled +testPatchDomainRegistration :: (HasCallStack) => FeatureTable -> App () +testPatchDomainRegistration table = checkPatchWithTable table OwnDomain "domainRegistration" enabled -testDomainRegistrationInternal :: (HasCallStack) => App () -testDomainRegistrationInternal = do +testDomainRegistrationInternal :: (HasCallStack) => FeatureTable -> App () +testDomainRegistrationInternal table = do (alice, tid, _) <- createTeam OwnDomain 0 + updateMigrationState OwnDomain tid table Internal.setTeamFeatureLockStatus alice tid "domainRegistration" "unlocked" withWebSocket alice $ \ws -> do setFlag InternalAPI ws tid "domainRegistration" enabled diff --git a/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs b/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs index 9bb1a608b4c..c61da4710c9 100644 --- a/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs +++ b/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs @@ -5,21 +5,22 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchEnforceFileDownloadLocation :: (HasCallStack) => App () -testPatchEnforceFileDownloadLocation = do - checkPatch OwnDomain "enforceFileDownloadLocation" +testPatchEnforceFileDownloadLocation :: (HasCallStack) => FeatureTable -> App () +testPatchEnforceFileDownloadLocation table = do + checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" $ object ["lockStatus" .= "unlocked"] - checkPatch OwnDomain "enforceFileDownloadLocation" + checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" $ object ["status" .= "enabled"] - checkPatch OwnDomain "enforceFileDownloadLocation" + checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" $ object ["lockStatus" .= "unlocked", "status" .= "enabled"] - checkPatch OwnDomain "enforceFileDownloadLocation" + checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" $ object ["lockStatus" .= "locked", "config" .= object []] - checkPatch OwnDomain "enforceFileDownloadLocation" + checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" $ object ["config" .= object ["enforcedDownloadLocation" .= "/tmp"]] do (user, tid, _) <- createTeam OwnDomain 0 + updateMigrationState OwnDomain tid table bindResponse ( Internal.patchTeamFeature user @@ -31,8 +32,8 @@ testPatchEnforceFileDownloadLocation = do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "empty-download-location" -testEnforceDownloadLocation :: (HasCallStack) => APIAccess -> App () -testEnforceDownloadLocation access = do +testEnforceDownloadLocation :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testEnforceDownloadLocation table access = do mkFeatureTests "enforceFileDownloadLocation" & addUpdate @@ -52,4 +53,5 @@ testEnforceDownloadLocation access = do ] ] ) + & setTable table & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/FileSharing.hs b/integration/test/Test/FeatureFlags/FileSharing.hs index 7cc761e64ef..eae3538511d 100644 --- a/integration/test/Test/FeatureFlags/FileSharing.hs +++ b/integration/test/Test/FeatureFlags/FileSharing.hs @@ -3,12 +3,13 @@ module Test.FeatureFlags.FileSharing where import Test.FeatureFlags.Util import Testlib.Prelude -testPatchFileSharing :: (HasCallStack) => App () -testPatchFileSharing = checkPatch OwnDomain "fileSharing" disabled +testPatchFileSharing :: (HasCallStack) => FeatureTable -> App () +testPatchFileSharing table = checkPatchWithTable table OwnDomain "fileSharing" disabled -testFileSharing :: (HasCallStack) => APIAccess -> App () -testFileSharing access = +testFileSharing :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testFileSharing table access = mkFeatureTests "fileSharing" & addUpdate disabled & addUpdate enabled + & setTable table & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/GuestLinks.hs b/integration/test/Test/FeatureFlags/GuestLinks.hs index 0c0c84ae387..8cccef6b83e 100644 --- a/integration/test/Test/FeatureFlags/GuestLinks.hs +++ b/integration/test/Test/FeatureFlags/GuestLinks.hs @@ -3,12 +3,13 @@ module Test.FeatureFlags.GuestLinks where import Test.FeatureFlags.Util import Testlib.Prelude -testConversationGuestLinks :: (HasCallStack) => APIAccess -> App () -testConversationGuestLinks access = +testConversationGuestLinks :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testConversationGuestLinks table access = mkFeatureTests "conversationGuestLinks" & addUpdate disabled & addUpdate enabled + & setTable table & runFeatureTests OwnDomain access -testPatchGuestLinks :: (HasCallStack) => App () -testPatchGuestLinks = checkPatch OwnDomain "conversationGuestLinks" disabled +testPatchGuestLinks :: (HasCallStack) => FeatureTable -> App () +testPatchGuestLinks table = checkPatchWithTable table OwnDomain "conversationGuestLinks" disabled diff --git a/integration/test/Test/FeatureFlags/Initialisation.hs b/integration/test/Test/FeatureFlags/Initialisation.hs index ac84a57ac91..ef62bb95661 100644 --- a/integration/test/Test/FeatureFlags/Initialisation.hs +++ b/integration/test/Test/FeatureFlags/Initialisation.hs @@ -5,11 +5,12 @@ import Control.Monad.Codensity import Control.Monad.Extra import Control.Monad.Reader import SetupHelpers +import Test.FeatureFlags.Util import Testlib.Prelude import Testlib.ResourcePool -testMLSInitialisation :: (HasCallStack) => App () -testMLSInitialisation = do +testMLSInitialisation :: (HasCallStack) => FeatureTable -> App () +testMLSInitialisation table = do let override = def { galleyCfg = @@ -42,6 +43,7 @@ testMLSInitialisation = do (alice, tid, _) <- createTeam domain 0 feat <- getTeamFeature alice tid "mls" >>= getJSON 200 feat %. "config.defaultProtocol" `shouldMatch` "proteus" + updateMigrationState domain tid table pure (alice, tid) lift $ lowerCodensity do diff --git a/integration/test/Test/FeatureFlags/LegalHold.hs b/integration/test/Test/FeatureFlags/LegalHold.hs index 45f099aef5c..fc523029826 100644 --- a/integration/test/Test/FeatureFlags/LegalHold.hs +++ b/integration/test/Test/FeatureFlags/LegalHold.hs @@ -9,8 +9,8 @@ import Test.FeatureFlags.Util import Testlib.Prelude import Testlib.ResourcePool (acquireResources) -testLegalholdDisabledByDefault :: (HasCallStack) => App () -testLegalholdDisabledByDefault = do +testLegalholdDisabledByDefault :: (HasCallStack) => FeatureTable -> App () +testLegalholdDisabledByDefault table = do let put uid tid st = Internal.setTeamFeatureConfig uid tid "legalhold" (object ["status" .= st]) >>= assertSuccess let patch uid tid st = Internal.setTeamFeatureStatus uid tid "legalhold" st >>= assertSuccess forM_ [put, patch] $ \setFeatureStatus -> do @@ -18,6 +18,7 @@ testLegalholdDisabledByDefault = do def {galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default"} $ \domain -> do (owner, tid, m : _) <- createTeam domain 2 + updateMigrationState domain tid table nonMember <- randomUser domain def assertForbidden =<< Public.getTeamFeature nonMember tid "legalhold" -- Test default @@ -29,8 +30,8 @@ testLegalholdDisabledByDefault = do checkFeature "legalhold" owner tid disabled -- always disabled -testLegalholdDisabledPermanently :: (HasCallStack) => App () -testLegalholdDisabledPermanently = do +testLegalholdDisabledPermanently :: (HasCallStack) => FeatureTable -> App () +testLegalholdDisabledPermanently table = do let cfgLhDisabledPermanently = def { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-permanently" @@ -46,6 +47,7 @@ testLegalholdDisabledPermanently = do -- Happy case: DB has no config for the team runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do (owner, tid, _) <- createTeam domain 1 + updateMigrationState domain tid table checkFeature "legalhold" owner tid disabled assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "enabled"]) @@ -54,6 +56,7 @@ testLegalholdDisabledPermanently = do -- changed to disabled-permanently (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do (owner, tid, _) <- createTeam domain 1 + updateMigrationState domain tid table checkFeature "legalhold" owner tid disabled assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" checkFeature "legalhold" owner tid enabled @@ -63,8 +66,8 @@ testLegalholdDisabledPermanently = do checkFeature "legalhold" owner tid disabled -- enabled if team is allow listed, disabled in any other case -testLegalholdWhitelistTeamsAndImplicitConsent :: (HasCallStack) => App () -testLegalholdWhitelistTeamsAndImplicitConsent = do +testLegalholdWhitelistTeamsAndImplicitConsent :: (HasCallStack) => FeatureTable -> App () +testLegalholdWhitelistTeamsAndImplicitConsent table = do let cfgLhWhitelistTeamsAndImplicitConsent = def { galleyCfg = setField "settings.featureFlags.legalhold" "whitelist-teams-and-implicit-consent" @@ -80,6 +83,7 @@ testLegalholdWhitelistTeamsAndImplicitConsent = do -- Happy case: DB has no config for the team (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do (owner, tid, _) <- createTeam domain 1 + updateMigrationState domain tid table checkFeature "legalhold" owner tid disabled Internal.legalholdWhitelistTeam tid owner >>= assertSuccess checkFeature "legalhold" owner tid enabled @@ -101,8 +105,8 @@ testLegalholdWhitelistTeamsAndImplicitConsent = do runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do checkFeature "legalhold" owner tid enabled -testExposeInvitationURLsToTeamAdminConfig :: (HasCallStack) => App () -testExposeInvitationURLsToTeamAdminConfig = do +testExposeInvitationURLsToTeamAdminConfig :: (HasCallStack) => FeatureTable -> App () +testExposeInvitationURLsToTeamAdminConfig table = do let cfgExposeInvitationURLsTeamAllowlist tids = def { galleyCfg = setField "settings.exposeInvitationURLsTeamAllowlist" tids @@ -114,6 +118,7 @@ testExposeInvitationURLsToTeamAdminConfig = do testNoAllowlistEntry :: (HasCallStack) => App (Value, String) testNoAllowlistEntry = runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist ([] :: [String])) $ \_ -> do (owner, tid, _) <- createTeam domain 1 + updateMigrationState domain tid table checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked -- here we get a response with HTTP status 200 and feature status unchanged (disabled), which we find weird, but we're just testing the current behavior -- a team that is not in the allow list cannot enable the feature, it will always be disabled and locked diff --git a/integration/test/Test/FeatureFlags/Mls.hs b/integration/test/Test/FeatureFlags/Mls.hs index 73cc96eaf12..941564ae555 100644 --- a/integration/test/Test/FeatureFlags/Mls.hs +++ b/integration/test/Test/FeatureFlags/Mls.hs @@ -4,8 +4,8 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testMls :: (HasCallStack) => APIAccess -> App () -testMls access = +testMls :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testMls table access = do user <- randomUser OwnDomain def uid <- asString $ user %. "id" @@ -13,10 +13,11 @@ testMls access = & addUpdate (mls1 uid) & addUpdate mls2 & addInvalidUpdate mlsInvalidConfig + & setTable table & runFeatureTests OwnDomain access -testMlsPatch :: (HasCallStack) => App () -testMlsPatch = do +testMlsPatch :: (HasCallStack) => FeatureTable -> App () +testMlsPatch table = do mlsMigrationDefaultConfig <- defAllFeatures %. "mlsMigration.config" withModifiedBackend def @@ -31,11 +32,11 @@ testMlsPatch = do ) } $ \domain -> do - checkPatch domain "mls" $ object ["lockStatus" .= "locked"] - checkPatch domain "mls" $ object ["status" .= "enabled"] - checkPatch domain "mls" + checkPatchWithTable table domain "mls" $ object ["lockStatus" .= "locked"] + checkPatchWithTable table domain "mls" $ object ["status" .= "enabled"] + checkPatchWithTable table domain "mls" $ object ["lockStatus" .= "locked", "status" .= "enabled"] - checkPatch domain "mls" + checkPatchWithTable table domain "mls" $ object [ "status" .= "enabled", "config" @@ -47,7 +48,7 @@ testMlsPatch = do "defaultCipherSuite" .= toJSON (1 :: Int) ] ] - checkPatch domain "mls" + checkPatchWithTable table domain "mls" $ object [ "config" .= object diff --git a/integration/test/Test/FeatureFlags/MlsE2EId.hs b/integration/test/Test/FeatureFlags/MlsE2EId.hs index dee32f94be2..2a5c06c9310 100644 --- a/integration/test/Test/FeatureFlags/MlsE2EId.hs +++ b/integration/test/Test/FeatureFlags/MlsE2EId.hs @@ -18,8 +18,8 @@ mlsE2EId1 = ] ] -testMLSE2EId :: (HasCallStack) => APIAccess -> App () -testMLSE2EId access = do +testMLSE2EId :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testMLSE2EId table access = do invalid <- mlsE2EId1 & if (access == InternalAPI) @@ -34,15 +34,16 @@ testMLSE2EId access = do & addUpdate mlsE2EId1 & addUpdate mlsE2EId2 & addInvalidUpdate invalid + & setTable table & runFeatureTests OwnDomain access -testPatchE2EId :: (HasCallStack) => App () -testPatchE2EId = do - checkPatch OwnDomain "mlsE2EId" (object ["lockStatus" .= "locked"]) - checkPatch OwnDomain "mlsE2EId" (object ["status" .= "enabled"]) - checkPatch OwnDomain "mlsE2EId" +testPatchE2EId :: (HasCallStack) => FeatureTable -> App () +testPatchE2EId table = do + checkPatchWithTable table OwnDomain "mlsE2EId" (object ["lockStatus" .= "locked"]) + checkPatchWithTable table OwnDomain "mlsE2EId" (object ["status" .= "enabled"]) + checkPatchWithTable table OwnDomain "mlsE2EId" $ object ["lockStatus" .= "locked", "status" .= "enabled"] - checkPatch OwnDomain "mlsE2EId" + checkPatchWithTable table OwnDomain "mlsE2EId" $ object [ "lockStatus" .= "unlocked", "config" @@ -53,7 +54,7 @@ testPatchE2EId = do ] ] - checkPatch OwnDomain "mlsE2EId" + checkPatchWithTable table OwnDomain "mlsE2EId" $ object [ "config" .= object @@ -63,9 +64,10 @@ testPatchE2EId = do ] ] -testMlsE2EConfigCrlProxyRequired :: (HasCallStack) => App () -testMlsE2EConfigCrlProxyRequired = do +testMlsE2EConfigCrlProxyRequired :: (HasCallStack) => FeatureTable -> App () +testMlsE2EConfigCrlProxyRequired table = do (owner, tid, _) <- createTeam OwnDomain 1 + updateMigrationState OwnDomain tid table let configWithoutCrlProxy = object [ "config" @@ -95,9 +97,10 @@ testMlsE2EConfigCrlProxyRequired = do expectedResponse <- configWithCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited" checkFeature "mlsE2EId" owner tid expectedResponse -testMlsE2EConfigCrlProxyNotRequiredInV5 :: (HasCallStack) => App () -testMlsE2EConfigCrlProxyNotRequiredInV5 = do +testMlsE2EConfigCrlProxyNotRequiredInV5 :: (HasCallStack) => FeatureTable -> App () +testMlsE2EConfigCrlProxyNotRequiredInV5 table = do (owner, tid, _) <- createTeam OwnDomain 1 + updateMigrationState OwnDomain tid table let configWithoutCrlProxy = object [ "config" diff --git a/integration/test/Test/FeatureFlags/MlsMigration.hs b/integration/test/Test/FeatureFlags/MlsMigration.hs index bac309fa5bb..8b5941e6781 100644 --- a/integration/test/Test/FeatureFlags/MlsMigration.hs +++ b/integration/test/Test/FeatureFlags/MlsMigration.hs @@ -7,10 +7,11 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testMlsMigration :: (HasCallStack) => APIAccess -> App () -testMlsMigration access = do +testMlsMigration :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testMlsMigration table access = do -- first we have to enable mls (owner, tid, _) <- createTeam OwnDomain 0 + updateMigrationState OwnDomain tid table void $ Public.setTeamFeatureConfig owner tid "mls" mlsEnable >>= getJSON 200 mkFeatureTests "mlsMigration" & addUpdate mlsMigrationConfig1 @@ -18,14 +19,15 @@ testMlsMigration access = do & setOwner owner >>= runFeatureTests OwnDomain access -testMlsMigrationDefaults :: (HasCallStack) => App () -testMlsMigrationDefaults = do +testMlsMigrationDefaults :: (HasCallStack) => FeatureTable -> App () +testMlsMigrationDefaults table = do withModifiedBackend def { galleyCfg = setField "settings.featureFlags.mlsMigration.defaults.lockStatus" "unlocked" } $ \domain -> do (owner, tid, _) <- createTeam domain 0 + updateMigrationState OwnDomain tid table void $ Internal.patchTeamFeature owner tid "mls" (object ["status" .= "enabled"]) >>= getJSON 200 diff --git a/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs b/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs index 8db8464a8d1..06c9aba1b34 100644 --- a/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs +++ b/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs @@ -3,12 +3,13 @@ module Test.FeatureFlags.OutlookCalIntegration where import Test.FeatureFlags.Util import Testlib.Prelude -testPatchOutlookCalIntegration :: (HasCallStack) => App () -testPatchOutlookCalIntegration = checkPatch OwnDomain "outlookCalIntegration" enabled +testPatchOutlookCalIntegration :: (HasCallStack) => FeatureTable -> App () +testPatchOutlookCalIntegration table = checkPatchWithTable table OwnDomain "outlookCalIntegration" enabled -testOutlookCalIntegration :: (HasCallStack) => APIAccess -> App () -testOutlookCalIntegration access = +testOutlookCalIntegration :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testOutlookCalIntegration table access = mkFeatureTests "outlookCalIntegration" & addUpdate enabled & addUpdate disabled + & setTable table & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/SSO.hs b/integration/test/Test/FeatureFlags/SSO.hs index 7b633ddcb10..2ddcef57877 100644 --- a/integration/test/Test/FeatureFlags/SSO.hs +++ b/integration/test/Test/FeatureFlags/SSO.hs @@ -6,8 +6,8 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testSSODisabledByDefault :: (HasCallStack) => App () -testSSODisabledByDefault = do +testSSODisabledByDefault :: (HasCallStack) => FeatureTable -> App () +testSSODisabledByDefault table = do let put uid tid = Internal.setTeamFeatureConfig uid tid "sso" (object ["status" .= "enabled"]) >>= assertSuccess let patch uid tid = Internal.setTeamFeatureStatus uid tid "sso" "enabled" >>= assertSuccess forM_ [put, patch] $ \enableFeature -> do @@ -15,6 +15,7 @@ testSSODisabledByDefault = do def {galleyCfg = setField "settings.featureFlags.sso" "disabled-by-default"} $ \domain -> do (owner, tid, m : _) <- createTeam domain 2 + updateMigrationState domain tid table nonMember <- randomUser domain def assertForbidden =<< Public.getTeamFeature nonMember tid "sso" -- Test default @@ -23,12 +24,13 @@ testSSODisabledByDefault = do enableFeature owner tid checkFeature "sso" owner tid enabled -testSSOEnabledByDefault :: (HasCallStack) => App () -testSSOEnabledByDefault = do +testSSOEnabledByDefault :: (HasCallStack) => FeatureTable -> App () +testSSOEnabledByDefault table = do withModifiedBackend def {galleyCfg = setField "settings.featureFlags.sso" "enabled-by-default"} $ \domain -> do (owner, tid, _m : _) <- createTeam domain 2 + updateMigrationState domain tid table nonMember <- randomUser domain def assertForbidden =<< Public.getTeamFeature nonMember tid "sso" checkFeature "sso" owner tid enabled diff --git a/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs b/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs index a2ce39cd44e..a21828d7966 100644 --- a/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs +++ b/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs @@ -6,13 +6,14 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchSearchVisibility :: (HasCallStack) => App () -testPatchSearchVisibility = checkPatch OwnDomain "searchVisibility" enabled +testPatchSearchVisibility :: (HasCallStack) => FeatureTable -> App () +testPatchSearchVisibility table = checkPatchWithTable table OwnDomain "searchVisibility" enabled -testSearchVisibilityDisabledByDefault :: (HasCallStack) => App () -testSearchVisibilityDisabledByDefault = do +testSearchVisibilityDisabledByDefault :: (HasCallStack) => FeatureTable -> App () +testSearchVisibilityDisabledByDefault table = do withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "disabled-by-default"} $ \domain -> do (owner, tid, m : _) <- createTeam domain 2 + updateMigrationState domain tid table -- Test default checkFeature "searchVisibility" m tid disabled assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" @@ -20,10 +21,11 @@ testSearchVisibilityDisabledByDefault = do assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" checkFeature "searchVisibility" owner tid disabled -testSearchVisibilityEnabledByDefault :: (HasCallStack) => App () -testSearchVisibilityEnabledByDefault = do +testSearchVisibilityEnabledByDefault :: (HasCallStack) => FeatureTable -> App () +testSearchVisibilityEnabledByDefault table = do withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} $ \domain -> do (owner, tid, m : _) <- createTeam domain 2 + updateMigrationState domain tid table nonMember <- randomUser domain def assertForbidden =<< Public.getTeamFeature nonMember tid "searchVisibility" -- Test default diff --git a/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs index c1ef7a5d3ca..2919d19010b 100644 --- a/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs +++ b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs @@ -5,10 +5,11 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testSearchVisibilityInboundInternal :: (HasCallStack) => APIAccess -> App () -testSearchVisibilityInboundInternal access = do +testSearchVisibilityInboundInternal :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testSearchVisibilityInboundInternal table access = do let featureName = "searchVisibilityInbound" (alice, tid, _) <- createTeam OwnDomain 2 + updateMigrationState OwnDomain tid table eve <- randomUser OwnDomain def assertForbidden =<< Public.getTeamFeature eve tid featureName checkFeature featureName alice tid disabled diff --git a/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs b/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs index 019bed20341..ee3954d02f4 100644 --- a/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs +++ b/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs @@ -13,23 +13,24 @@ feature ps timeout = ] ) -testSelfDeletingMessages :: (HasCallStack) => APIAccess -> App () -testSelfDeletingMessages access = +testSelfDeletingMessages :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testSelfDeletingMessages table access = mkFeatureTests "selfDeletingMessages" & addUpdate (feature ["status" .= "disabled"] (0 :: Int)) & addUpdate (feature ["status" .= "enabled"] (30 :: Int)) & addInvalidUpdate (feature ["status" .= "enabled"] "") + & setTable table & runFeatureTests OwnDomain access -testPatchSelfDeletingMessages :: (HasCallStack) => App () -testPatchSelfDeletingMessages = do - checkPatch OwnDomain "selfDeletingMessages" +testPatchSelfDeletingMessages :: (HasCallStack) => FeatureTable -> App () +testPatchSelfDeletingMessages table = do + checkPatchWithTable table OwnDomain "selfDeletingMessages" $ object ["lockStatus" .= "locked"] - checkPatch OwnDomain "selfDeletingMessages" + checkPatchWithTable table OwnDomain "selfDeletingMessages" $ object ["status" .= "disabled"] - checkPatch OwnDomain "selfDeletingMessages" + checkPatchWithTable table OwnDomain "selfDeletingMessages" $ object ["lockStatus" .= "locked", "status" .= "disabled"] - checkPatch OwnDomain "selfDeletingMessages" + checkPatchWithTable table OwnDomain "selfDeletingMessages" $ object ["lockStatus" .= "unlocked", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]] - checkPatch OwnDomain "selfDeletingMessages" + checkPatchWithTable table OwnDomain "selfDeletingMessages" $ object ["config" .= object ["enforcedTimeoutSeconds" .= A.Number 60]] diff --git a/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs b/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs index 7acc3621f4e..8fb976ec8d1 100644 --- a/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs +++ b/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs @@ -3,14 +3,15 @@ module Test.FeatureFlags.SndFactorPasswordChallenge where import Test.FeatureFlags.Util import Testlib.Prelude -testPatchSndFactorPasswordChallenge :: (HasCallStack) => App () -testPatchSndFactorPasswordChallenge = - checkPatch OwnDomain "sndFactorPasswordChallenge" enabled +testPatchSndFactorPasswordChallenge :: (HasCallStack) => FeatureTable -> App () +testPatchSndFactorPasswordChallenge table = + checkPatchWithTable table OwnDomain "sndFactorPasswordChallenge" enabled -testSndFactorPasswordChallenge :: (HasCallStack) => APIAccess -> App () -testSndFactorPasswordChallenge access = +testSndFactorPasswordChallenge :: (HasCallStack) => FeatureTable -> APIAccess -> App () +testSndFactorPasswordChallenge table access = do mkFeatureTests "sndFactorPasswordChallenge" & addUpdate enabled & addUpdate disabled + & setTable table & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 402a97be60d..114b80fcd65 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -218,8 +218,19 @@ checkPatch :: String -> Value -> App () -checkPatch domain featureName patch = do +checkPatch = checkPatchWithTable FeatureTableLegacy + +checkPatchWithTable :: + (HasCallStack, MakesValue domain) => + FeatureTable -> + domain -> + String -> + Value -> + App () +checkPatchWithTable table domain featureName patch = do (owner, tid, _) <- createTeam domain 0 + updateMigrationState domain tid table + defFeature <- defAllFeatures %. featureName let valueOrDefault :: String -> App Value @@ -267,11 +278,12 @@ data FeatureTests = FeatureTests -- payload) updates :: [Value], invalidUpdates :: [Value], - owner :: Maybe Value + owner :: Maybe Value, + table :: FeatureTable } mkFeatureTests :: String -> FeatureTests -mkFeatureTests name = FeatureTests name [] [] Nothing +mkFeatureTests name = FeatureTests name [] [] Nothing FeatureTableLegacy addUpdate :: Value -> FeatureTests -> FeatureTests addUpdate up ft = ft {updates = ft.updates <> [up]} @@ -284,6 +296,9 @@ setOwner owner ft = do x <- make owner pure ft {owner = Just x} +setTable :: FeatureTable -> FeatureTests -> FeatureTests +setTable table ft = ft {table = table} + runFeatureTests :: (HasCallStack, MakesValue domain) => domain -> diff --git a/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs b/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs index 6177c52be87..fcd945460e8 100644 --- a/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs +++ b/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs @@ -4,14 +4,15 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchValidateSAMLEmails :: (HasCallStack) => App () -testPatchValidateSAMLEmails = - checkPatch OwnDomain "validateSAMLemails" +testPatchValidateSAMLEmails :: (HasCallStack) => FeatureTable -> App () +testPatchValidateSAMLEmails table = + checkPatchWithTable table OwnDomain "validateSAMLemails" $ object ["status" .= "disabled"] -testValidateSAMLEmailsInternal :: (HasCallStack) => App () -testValidateSAMLEmailsInternal = do +testValidateSAMLEmailsInternal :: (HasCallStack) => FeatureTable -> App () +testValidateSAMLEmailsInternal table = do (alice, tid, _) <- createTeam OwnDomain 0 + updateMigrationState OwnDomain tid table withWebSocket alice $ \ws -> do setFlag InternalAPI ws tid "validateSAMLemails" disabled setFlag InternalAPI ws tid "validateSAMLemails" enabled From 4e95f6e014698fc31e925c907888bf3da72be930 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 19 Feb 2025 14:29:35 +0000 Subject: [PATCH 10/25] test read only while migration is in progress --- integration/test/API/GalleyInternal.hs | 8 ++- integration/test/Test/FeatureFlags/AppLock.hs | 5 ++ .../Test/FeatureFlags/ConferenceCalling.hs | 12 ++++ .../test/Test/FeatureFlags/GuestLinks.hs | 10 ++++ integration/test/Test/FeatureFlags/Mls.hs | 9 +++ integration/test/Test/FeatureFlags/Util.hs | 60 +++++++++++++++++++ 6 files changed, 102 insertions(+), 2 deletions(-) diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index 4fee51bf960..45ada18cd11 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -47,10 +47,14 @@ setTeamFeatureStatus domain team featureName status = do setTeamFeatureLockStatus :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> App () setTeamFeatureLockStatus domain team featureName status = do + bindResponse (setTeamFeatureLockStatusResponse domain team featureName status) $ \res -> + res.status `shouldMatchInt` 200 + +setTeamFeatureLockStatusResponse :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> App Response +setTeamFeatureLockStatusResponse domain team featureName status = do tid <- asString team req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName, status] - bindResponse (submit "PUT" $ req) $ \res -> - res.status `shouldMatchInt` 200 + submit "PUT" $ req getFederationStatus :: ( HasCallStack, diff --git a/integration/test/Test/FeatureFlags/AppLock.hs b/integration/test/Test/FeatureFlags/AppLock.hs index 95600c2807b..bd6d5be79f4 100644 --- a/integration/test/Test/FeatureFlags/AppLock.hs +++ b/integration/test/Test/FeatureFlags/AppLock.hs @@ -29,3 +29,8 @@ testPatchAppLock table = do "inactivityTimeoutSecs" .= A.Number 240 ] ] + +testPatchAppLockReadOnly :: (HasCallStack) => App () +testPatchAppLockReadOnly = do + checkPatchReadOnly OwnDomain "appLock" + $ object ["lockStatus" .= "locked"] diff --git a/integration/test/Test/FeatureFlags/ConferenceCalling.hs b/integration/test/Test/FeatureFlags/ConferenceCalling.hs index 30cc5621bcc..34e80ac3d0b 100644 --- a/integration/test/Test/FeatureFlags/ConferenceCalling.hs +++ b/integration/test/Test/FeatureFlags/ConferenceCalling.hs @@ -24,3 +24,15 @@ testConferenceCalling access = do & addUpdate (confCalling def {sft = toJSON True}) & addUpdate (confCalling def {sft = toJSON False}) & addInvalidUpdate (confCalling def {sft = toJSON (0 :: Int)}) + +testPatchConferenceCallingReadOnly :: (HasCallStack) => App () +testPatchConferenceCallingReadOnly = do + checkPatchReadOnly OwnDomain "conferenceCalling" + $ object ["lockStatus" .= "locked"] + +testConferenceCallingReadOnlyDuringMigration :: (HasCallStack) => APIAccess -> App () +testConferenceCallingReadOnlyDuringMigration access = do + runFeatureTestsReadOnly OwnDomain access + $ mkFeatureTests "conferenceCalling" + & addUpdate (confCalling def {sft = toJSON True}) + & addUpdate (confCalling def {sft = toJSON False}) diff --git a/integration/test/Test/FeatureFlags/GuestLinks.hs b/integration/test/Test/FeatureFlags/GuestLinks.hs index 8cccef6b83e..9c1d513779f 100644 --- a/integration/test/Test/FeatureFlags/GuestLinks.hs +++ b/integration/test/Test/FeatureFlags/GuestLinks.hs @@ -13,3 +13,13 @@ testConversationGuestLinks table access = testPatchGuestLinks :: (HasCallStack) => FeatureTable -> App () testPatchGuestLinks table = checkPatchWithTable table OwnDomain "conversationGuestLinks" disabled + +testConversationGuestLinksReadOnly :: (HasCallStack) => APIAccess -> App () +testConversationGuestLinksReadOnly access = + runFeatureTestsReadOnly OwnDomain access + $ mkFeatureTests "conversationGuestLinks" + & addUpdate disabled + & addUpdate enabled + +testPatchGuestLinksReadOnly :: (HasCallStack) => App () +testPatchGuestLinksReadOnly = checkPatchReadOnly OwnDomain "conversationGuestLinks" disabled diff --git a/integration/test/Test/FeatureFlags/Mls.hs b/integration/test/Test/FeatureFlags/Mls.hs index 941564ae555..34b042dfcd7 100644 --- a/integration/test/Test/FeatureFlags/Mls.hs +++ b/integration/test/Test/FeatureFlags/Mls.hs @@ -60,6 +60,15 @@ testMlsPatch table = do ] ] +testMlsReadOnly :: (HasCallStack) => APIAccess -> App () +testMlsReadOnly access = + runFeatureTestsReadOnly OwnDomain access + $ mkFeatureTests "mls" + & addUpdate mls2 + +testPatchMlsReadOnly :: (HasCallStack) => App () +testPatchMlsReadOnly = checkPatchReadOnly OwnDomain "mls" mls2 + mls1 :: String -> Value mls1 uid = object diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 114b80fcd65..daa087bb7fc 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -369,3 +369,63 @@ updateMigrationState domain tid ft = case ft of FeatureTableLegacy -> pure () FeatureTableDyn -> do Internal.setTeamFeatureMigrationState domain tid "completed" >>= assertSuccess + +runFeatureTestsReadOnly :: + (HasCallStack, MakesValue domain) => + domain -> + APIAccess -> + FeatureTests -> + App () +runFeatureTestsReadOnly domain access ft = do + defFeature <- defAllFeatures %. ft.name + do + user <- randomUser domain def + bindResponse (Public.getFeatureConfigs user) $ \resp -> do + resp.status `shouldMatchInt` 200 + feat <- resp.json %. ft.name + lockStatus <- feat %. "lockStatus" + expected <- setField "lockStatus" lockStatus defFeature + feat `shouldMatch` expected + + (owner, tid, _) <- createTeam domain 0 + + checkFeature ft.name owner tid defFeature + + -- unlock the feature + Internal.setTeamFeatureLockStatus owner tid ft.name "unlocked" + + -- set migration state to in progress + void $ Internal.setTeamFeatureMigrationState domain tid "in_progress" + featureStatus <- Internal.getTeamFeature domain tid ft.name >>= getJSON 200 + + -- locking the feature should not work + Internal.setTeamFeatureLockStatusResponse owner tid ft.name "locked" `bindResponse` assertMigrationInProgress + + -- updates do not work + for_ ft.updates $ \u -> do + setFeature access owner tid ft.name u `bindResponse` assertMigrationInProgress + + checkFeature ft.name owner tid featureStatus + +assertMigrationInProgress :: (HasCallStack) => Response -> App () +assertMigrationInProgress res = do + res.status `shouldMatchInt` 500 + res.json %. "label" `shouldMatch` "internal-error" + res.json %. "message" `shouldMatch` "migration in progress" + +checkPatchReadOnly :: + (HasCallStack, MakesValue domain) => + domain -> + String -> + Value -> + App () +checkPatchReadOnly domain featureName patch = do + (owner, tid, _) <- createTeam domain 0 + void $ Internal.setTeamFeatureMigrationState domain tid "in_progress" + defFeature <- defAllFeatures %. featureName + + checkFeature featureName owner tid defFeature + Internal.patchTeamFeature domain tid featureName patch + `bindResponse` assertMigrationInProgress + + checkFeature featureName owner tid defFeature From 0a2784d7bdc028213651d3c843395f4f297e44e0 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 17 Feb 2025 14:51:43 +0000 Subject: [PATCH 11/25] Import data migration --- services/galley/galley.cabal | 4 + services/galley/migrate-data/src/Run.hs | 4 +- .../src/V4_MigrateToDynamicFeatures.hs | 395 ++++++++++++++++++ 3 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 558ce09a4e8..df7cb97a3aa 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -567,17 +567,21 @@ executable galley-migrate-data Run V1_BackfillBillingTeamMembers V3_BackfillTeamAdmins + V4_MigrateToDynamicFeatures hs-source-dirs: migrate-data/src build-depends: + , barbies , base , cassandra-util , conduit , containers + , data-default , exceptions , extended , imports , optparse-applicative + , schema-profunctor , text , time , tinylog diff --git a/services/galley/migrate-data/src/Run.hs b/services/galley/migrate-data/src/Run.hs index bf85d0e97d2..5c16b5c0081 100644 --- a/services/galley/migrate-data/src/Run.hs +++ b/services/galley/migrate-data/src/Run.hs @@ -23,6 +23,7 @@ import Options.Applicative import System.Logger.Extended qualified as Log import V1_BackfillBillingTeamMembers qualified import V3_BackfillTeamAdmins qualified +import V4_MigrateToDynamicFeatures qualified main :: IO () main = do @@ -32,7 +33,8 @@ main = do l o [ V1_BackfillBillingTeamMembers.migration, - V3_BackfillTeamAdmins.migration + V3_BackfillTeamAdmins.migration, + V4_MigrateToDynamicFeatures.migration ] where desc = header "Galley Cassandra Data Migrations" <> fullDesc diff --git a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs new file mode 100644 index 00000000000..2695a948f46 --- /dev/null +++ b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs @@ -0,0 +1,395 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module V4_MigrateToDynamicFeatures where + +import Barbies.Bare +import Cassandra +import Cassandra qualified as C +import Conduit +import Data.Conduit.List qualified as C +import Data.Default +import Data.Id +import Data.Misc +import Data.Schema +import Data.Time.Clock +import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) +import Galley.DataMigration.Types +import Imports +import Wire.API.Conversation.Protocol +import Wire.API.MLS.CipherSuite +import Wire.API.Team.Feature + +migration :: Migration +migration = + Migration + { version = MigrationVersion 4, + text = "Migrate to dynamic features", + action = runConduit $ getFeatures .| C.concat .| C.mapM_ writeFeatures + } + +pageSize :: Int32 +pageSize = 1000 + +---------------------------------------------------------------------------- +-- Combined Row Type + +type FeatureRow = + ( TeamId, + Maybe EnforceAppLock, -- app_lock_enforce + Maybe Int32, -- app_lock_inactivity_timeout_secs + Maybe FeatureStatus, -- app_lock_status + Maybe LockStatus, -- conference_calling + Maybe One2OneCalls, -- conference_calling_one_to_one + Maybe FeatureStatus, -- conference_calling_status + Maybe FeatureStatus, -- digital_signatures + Maybe LockStatus, -- domain_registration_lock_status + Maybe FeatureStatus, -- domain_registration_status + Maybe Text, -- enforce_file_download_location + Maybe LockStatus, -- enforce_file_download_location_lock_status + Maybe FeatureStatus, -- enforce_file_download_location_status + Maybe FeatureStatus, -- expose_invitation_urls_to_team_admin + Maybe FeatureStatus, -- file_sharing + Maybe LockStatus, -- file_sharing_lock_status + Maybe LockStatus, -- guest_links_lock_status + Maybe FeatureStatus, -- guest_links_status + Maybe FeatureStatus, -- legalhold_status + Maybe FeatureStatus, -- limited_event_fanout_status + Maybe (Cassandra.Set CipherSuiteTag), -- mls_allowed_ciphersuites + Maybe CipherSuiteTag, -- mls_default_ciphersuite + Maybe ProtocolTag, -- mls_default_protocol + Maybe HttpsUrl, -- mls_e2eid_acme_discovery_url + Maybe HttpsUrl, -- mls_e2eid_crl_proxy + Maybe Int32, -- mls_e2eid_grace_period + Maybe LockStatus, -- mls_e2eid_lock_status + Maybe FeatureStatus, -- mls_e2eid_status + Maybe Bool, -- mls_e2eid_use_proxy_on_mobile + Maybe LockStatus, -- mls_lock_status + Maybe OptionalUTCTime, -- mls_migration_finalise_regardless_after + Maybe LockStatus, -- mls_migration_lock_status + Maybe OptionalUTCTime, -- mls_migration_start_time + Maybe FeatureStatus, -- mls_migration_status + Maybe (Cassandra.Set UserId), -- mls_protocol_toggle_users + Maybe FeatureStatus, -- mls_status + Maybe (Cassandra.Set ProtocolTag), -- mls_supported_protocols + Maybe LockStatus, -- outlook_cal_integration_lock_status + Maybe FeatureStatus, -- outlook_cal_integration_status + Maybe FeatureStatus, -- search_visibility_inbound_status + Maybe FeatureStatus, -- search_visibility_status + Maybe LockStatus, -- self_deleting_messages_lock_status + Maybe FeatureStatus, -- self_deleting_messages_status + Maybe Int32, -- self_deleting_messages_ttl + Maybe LockStatus, -- snd_factor_password_challenge_lock_status + Maybe FeatureStatus, -- snd_factor_password_challenge_status + Maybe FeatureStatus, -- sso_status + Maybe FeatureStatus -- validate_saml_emails + ) + +---------------------------------------------------------------------------- +-- Query + +rowQuery :: PrepQuery R () FeatureRow +rowQuery = + "select \ + \ team_id, \ + \ app_lock_enforce, \ + \ app_lock_inactivity_timeout_secs, \ + \ app_lock_status, \ + \ conference_calling, \ + \ conference_calling_one_to_one, \ + \ conference_calling_status, \ + \ digital_signatures, \ + \ domain_registration_lock_status, \ + \ domain_registration_status, \ + \ enforce_file_download_location, \ + \ enforce_file_download_location_lock_status, \ + \ enforce_file_download_location_status, \ + \ expose_invitation_urls_to_team_admin, \ + \ file_sharing, \ + \ file_sharing_lock_status, \ + \ guest_links_lock_status, \ + \ guest_links_status, \ + \ legalhold_status, \ + \ limited_event_fanout_status, \ + \ mls_allowed_ciphersuites, \ + \ mls_default_ciphersuite, \ + \ mls_default_protocol, \ + \ mls_e2eid_acme_discovery_url, \ + \ mls_e2eid_crl_proxy, \ + \ mls_e2eid_grace_period, \ + \ mls_e2eid_lock_status, \ + \ mls_e2eid_status, \ + \ mls_e2eid_use_proxy_on_mobile, \ + \ mls_lock_status, \ + \ mls_migration_finalise_regardless_after, \ + \ mls_migration_lock_status, \ + \ mls_migration_start_time, \ + \ mls_migration_status, \ + \ mls_protocol_toggle_users, \ + \ mls_status, \ + \ mls_supported_protocols, \ + \ outlook_cal_integration_lock_status, \ + \ outlook_cal_integration_status, \ + \ search_visibility_inbound_status, \ + \ search_visibility_status, \ + \ self_deleting_messages_lock_status, \ + \ self_deleting_messages_status, \ + \ self_deleting_messages_ttl, \ + \ snd_factor_password_challenge_lock_status, \ + \ snd_factor_password_challenge_status, \ + \ sso_status, \ + \ validate_saml_emails \ + \ from team_features" + +writeFeatures :: + (MonadClient m) => + FeatureRow -> + m () +writeFeatures + ( team_id, + app_lock_enforce, + app_lock_inactivity_timeout_secs, + app_lock_status, + conference_calling, + conference_calling_one_to_one, + conference_calling_status, + digital_signatures, + domain_registration_lock_status, + domain_registration_status, + enforce_file_download_location, + enforce_file_download_location_lock_status, + enforce_file_download_location_status, + expose_invitation_urls_to_team_admin, + file_sharing, + file_sharing_lock_status, + guest_links_lock_status, + guest_links_status, + legalhold_status, + limited_event_fanout_status, + mls_allowed_ciphersuites, + mls_default_ciphersuite, + mls_default_protocol, + mls_e2eid_acme_discovery_url, + mls_e2eid_crl_proxy, + mls_e2eid_grace_period, + mls_e2eid_lock_status, + mls_e2eid_status, + mls_e2eid_use_proxy_on_mobile, + mls_lock_status, + mls_migration_finalise_regardless_after, + mls_migration_lock_status, + mls_migration_start_time, + mls_migration_status, + mls_protocol_toggle_users, + mls_status, + mls_supported_protocols, + outlook_cal_integration_lock_status, + outlook_cal_integration_status, + search_visibility_inbound_status, + search_visibility_status, + self_deleting_messages_lock_status, + self_deleting_messages_status, + self_deleting_messages_ttl, + snd_factor_password_challenge_lock_status, + snd_factor_password_challenge_status, + sso_status, + validate_saml_emails + ) = do + writeFeature @AppLockConfig team_id $ + (def :: LockableFeaturePatch DbConfig) + { status = app_lock_status, + config = + Just . DbConfig $ + schemaToJSON (AppLockConfig @Covered app_lock_enforce app_lock_inactivity_timeout_secs) + } + writeFeature @ConferenceCallingConfig team_id $ + LockableFeaturePatch + { status = conference_calling_status, + lockStatus = conference_calling, + config = + DbConfig . schemaToJSON . ConferenceCallingConfig <$> conference_calling_one_to_one + } + + writeFeature @DigitalSignaturesConfig team_id $ + (def :: LockableFeaturePatch DbConfig) {status = digital_signatures} + + writeFeature @DomainRegistrationConfig team_id $ + (def :: LockableFeaturePatch DbConfig) + { status = domain_registration_status, + lockStatus = domain_registration_lock_status + } + + writeFeature @EnforceFileDownloadLocationConfig team_id $ + LockableFeaturePatch + { status = enforce_file_download_location_status, + lockStatus = enforce_file_download_location_lock_status, + config = + Just . DbConfig . schemaToJSON . EnforceFileDownloadLocationConfig $ + enforce_file_download_location + } + + writeFeature @ExposeInvitationURLsToTeamAdminConfig team_id $ + (def :: LockableFeaturePatch DbConfig) {status = expose_invitation_urls_to_team_admin} + + writeFeature @FileSharingConfig team_id $ + (def :: LockableFeaturePatch DbConfig) + { status = file_sharing, + lockStatus = file_sharing_lock_status + } + + writeFeature @GuestLinksConfig team_id $ + (def :: LockableFeaturePatch DbConfig) + { status = guest_links_status, + lockStatus = guest_links_lock_status + } + writeFeature @LegalholdConfig team_id $ + (def :: LockableFeaturePatch DbConfig) {status = legalhold_status} + + writeFeature @LimitedEventFanoutConfig team_id $ + (def :: LockableFeaturePatch DbConfig) {status = limited_event_fanout_status} + + writeFeature @MLSConfig team_id $ + LockableFeaturePatch + { status = mls_status, + lockStatus = mls_lock_status, + config = + Just . DbConfig $ + schemaToJSON + ( MLSConfig @Covered + (fmap C.fromSet mls_protocol_toggle_users) + mls_default_protocol + (fmap C.fromSet mls_allowed_ciphersuites) + mls_default_ciphersuite + (fmap C.fromSet mls_supported_protocols) + ) + } + + writeFeature @MlsE2EIdConfig team_id $ + LockableFeaturePatch + { status = mls_e2eid_status, + lockStatus = mls_e2eid_lock_status, + config = + Just . DbConfig $ + schemaToJSON + ( MlsE2EIdConfig @Covered + (fmap fromIntegral mls_e2eid_grace_period) + (Alt mls_e2eid_acme_discovery_url) + (Alt mls_e2eid_crl_proxy) + (maybe def UseProxyOnMobile mls_e2eid_use_proxy_on_mobile) + ) + } + + writeFeature @MlsMigrationConfig team_id $ + LockableFeaturePatch + { status = mls_migration_status, + lockStatus = mls_migration_lock_status, + config = + Just . DbConfig $ + schemaToJSON + ( MlsMigrationConfig @Covered + (fmap unOptionalUTCTime mls_migration_start_time) + (fmap unOptionalUTCTime mls_migration_finalise_regardless_after) + ) + } + + writeFeature @OutlookCalIntegrationConfig team_id $ + (def :: LockableFeaturePatch DbConfig) + { status = outlook_cal_integration_status, + lockStatus = outlook_cal_integration_lock_status + } + + writeFeature @SearchVisibilityInboundConfig team_id $ + (def :: LockableFeaturePatch DbConfig) + { status = search_visibility_inbound_status + } + + writeFeature @SearchVisibilityAvailableConfig team_id $ + (def :: LockableFeaturePatch DbConfig) + { status = search_visibility_status + } + + writeFeature @SelfDeletingMessagesConfig team_id $ + LockableFeaturePatch + { status = self_deleting_messages_status, + lockStatus = self_deleting_messages_lock_status, + config = + DbConfig + . schemaToJSON + . SelfDeletingMessagesConfig + <$> self_deleting_messages_ttl + } + + writeFeature @SndFactorPasswordChallengeConfig team_id $ + (def :: LockableFeaturePatch DbConfig) + { status = snd_factor_password_challenge_status, + lockStatus = snd_factor_password_challenge_lock_status + } + + writeFeature @SSOConfig team_id $ + (def :: LockableFeaturePatch DbConfig) {status = sso_status} + + writeFeature @ValidateSAMLEmailsConfig team_id $ + (def :: LockableFeaturePatch DbConfig) {status = validate_saml_emails} + +---------------------------------------------------------------------------- + +-- Pagination + +getFeatures :: (MonadClient m) => ConduitM () [FeatureRow] m () +getFeatures = paginateC rowQuery (paramsP LocalQuorum () pageSize) x5 + +---------------------------------------------------------------------------- +-- Instances (unchanged) + +instance Cql EnforceAppLock where + ctype = Tagged IntColumn + toCql (EnforceAppLock False) = CqlInt 0 + toCql (EnforceAppLock True) = CqlInt 1 + fromCql (CqlInt n) = case n of + 0 -> pure (EnforceAppLock False) + 1 -> pure (EnforceAppLock True) + _ -> Left "fromCql EnforceAppLock: int out of range" + fromCql _ = Left "fromCql EnforceAppLock: int expected" + +instance Cql ProtocolTag where + ctype = Tagged IntColumn + toCql = CqlInt . fromIntegral . fromEnum + fromCql (CqlInt i) = do + let i' = fromIntegral i + if i' < fromEnum @ProtocolTag minBound + || i' > fromEnum @ProtocolTag maxBound + then Left $ "unexpected protocol: " ++ show i + else Right $ toEnum i' + fromCql _ = Left "protocol: int expected" + +-- Optional time stamp. A 'Nothing' value is represented as 0. +newtype OptionalUTCTime = OptionalUTCTime {unOptionalUTCTime :: Maybe UTCTime} + +instance Cql OptionalUTCTime where + ctype = Tagged (untag (ctype @UTCTime)) + toCql = toCql . fromMaybe (posixSecondsToUTCTime 0) . unOptionalUTCTime + fromCql x = do + t <- fromCql x + pure . OptionalUTCTime $ guard (utcTimeToPOSIXSeconds t /= 0) $> t + +writeFeature :: + forall cfg m. + (IsFeatureConfig cfg, MonadClient m) => + TeamId -> + LockableFeaturePatch DbConfig -> + m () +writeFeature tid feat = do + let q :: PrepQuery W (Maybe FeatureStatus, Maybe LockStatus, Maybe DbConfig, TeamId, Text) () + q = "update team_features_dyn set status = ?, lock_status = ?, config = ? where team = ? and feature = ?" + retry x5 $ + write + q + ( params + LocalQuorum + ( feat.status, + feat.lockStatus, + feat.config, + tid, + featureName @cfg + ) + ) From 42cfc79bb4ca0aeb8b0b6cc5b6fd32aa7848bfa0 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 20 Feb 2025 09:26:09 +0100 Subject: [PATCH 12/25] Convert ConferenceCalling to a barbie feature --- libs/wire-api/src/Wire/API/Team/Feature.hs | 61 +++++++++++-------- .../src/Wire/API/Team/Feature/Profunctor.hs | 53 ++++++++++++++++ libs/wire-api/src/Wire/API/Team/Feature/TH.hs | 16 +++++ libs/wire-api/wire-api.cabal | 2 + .../src/V4_MigrateToDynamicFeatures.hs | 3 +- 5 files changed, 108 insertions(+), 27 deletions(-) create mode 100644 libs/wire-api/src/Wire/API/Team/Feature/Profunctor.hs diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 5b4fbc94985..0e4728a8985 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -60,7 +60,8 @@ module Wire.API.Team.Feature SelfDeletingMessagesConfig (..), ValidateSAMLEmailsConfig (..), DigitalSignaturesConfig (..), - ConferenceCallingConfig (..), + ConferenceCallingConfigB (..), + ConferenceCallingConfig, GuestLinksConfig (..), ExposeInvitationURLsToTeamAdminConfig (..), SndFactorPasswordChallengeConfig (..), @@ -137,6 +138,7 @@ import Test.QuickCheck.Gen (suchThat) import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.Routes.Named hiding (unnamed) +import Wire.API.Team.Feature.Profunctor import Wire.Arbitrary (Arbitrary, GenericUniform (..)) ---------------------------------------------------------------------- @@ -729,6 +731,9 @@ one2OneCallsFromUseSftFlag :: Bool -> One2OneCalls one2OneCallsFromUseSftFlag False = One2OneCallsTurn one2OneCallsFromUseSftFlag True = One2OneCallsSft +one2OneCallsSchema :: ValueSchema SwaggerDoc One2OneCalls +one2OneCallsSchema = one2OneCallsFromUseSftFlag <$> (== One2OneCallsSft) .= unnamed schema + instance Default One2OneCalls where def = One2OneCallsTurn @@ -744,13 +749,31 @@ instance Cass.Cql One2OneCalls where toCql One2OneCallsTurn = Cass.CqlInt 0 toCql One2OneCallsSft = Cass.CqlInt 1 -data ConferenceCallingConfig = ConferenceCallingConfig - { one2OneCalls :: One2OneCalls +data ConferenceCallingConfigB t f = ConferenceCallingConfig + { one2OneCalls :: Wear t f One2OneCalls } - deriving (Eq, Show, Generic, GSOP.Generic) - deriving (Arbitrary) via (GenericUniform ConferenceCallingConfig) - deriving (RenderableSymbol) via (RenderableTypeName ConferenceCallingConfig) - deriving (ParseDbFeature, Default) via (SimpleFeature ConferenceCallingConfig) + deriving (BareB, Generic) + +deriving instance FunctorB (ConferenceCallingConfigB Covered) + +deriving instance ApplicativeB (ConferenceCallingConfigB Covered) + +type ConferenceCallingConfig = ConferenceCallingConfigB Bare Identity + +deriving instance (Eq ConferenceCallingConfig) + +deriving instance (Show ConferenceCallingConfig) + +deriving via (GenericUniform ConferenceCallingConfig) instance (Arbitrary ConferenceCallingConfig) + +deriving via (RenderableTypeName ConferenceCallingConfig) instance (RenderableSymbol ConferenceCallingConfig) + +deriving via (BarbieFeature ConferenceCallingConfigB) instance (ParseDbFeature ConferenceCallingConfig) + +deriving via (BarbieFeature ConferenceCallingConfigB) instance (ToSchema ConferenceCallingConfig) + +instance Default ConferenceCallingConfig where + def = ConferenceCallingConfig def instance Default (LockableFeature ConferenceCallingConfig) where def = defLockedFeature {status = FeatureStatusEnabled} @@ -760,14 +783,13 @@ instance IsFeatureConfig ConferenceCallingConfig where featureSingleton = FeatureSingletonConferenceCallingConfig objectSchema = fromMaybe def <$> optField "config" schema -instance ToSchema ConferenceCallingConfig where +instance (OptWithDefault f) => ToSchema (ConferenceCallingConfigB Covered f) where schema = object "ConferenceCallingConfig" $ ConferenceCallingConfig - <$> ((== One2OneCallsSft) . one2OneCalls) - .= ( maybe def one2OneCallsFromUseSftFlag - <$> optField "useSFTForOneToOneCalls" schema - ) + <$> one2OneCalls + .= fromOpt + (optField "useSFTForOneToOneCalls" one2OneCallsSchema) -------------------------------------------------------------------------------- -- SndFactorPasswordChallenge feature @@ -1181,20 +1203,7 @@ instance Arbitrary MlsMigrationConfig where finaliseRegardlessAfter = finaliseRegardlessAfter } --- | This class enables non-standard JSON instances for the Identity case of --- this feature. For backwards compatibility, we need to make the two fields --- optional even in the Identity case. A missing field gets parsed as --- `Nothing`. Whereas with the default instance, they would be rejected. -class NestedMaybeFieldFunctor f where - nestedMaybeField :: Text -> ValueSchema SwaggerDoc a -> ObjectSchema SwaggerDoc (f (Maybe a)) - -instance NestedMaybeFieldFunctor Maybe where - nestedMaybeField name sch = maybe_ (optField name (nullable sch)) - -instance NestedMaybeFieldFunctor Identity where - nestedMaybeField name sch = Identity <$> runIdentity .= maybe_ (optField name sch) - -instance (NestedMaybeFieldFunctor f) => ToSchema (MlsMigrationConfigB Covered f) where +instance (NestedMaybe f) => ToSchema (MlsMigrationConfigB Covered f) where schema = object "MlsMigration" $ MlsMigrationConfig diff --git a/libs/wire-api/src/Wire/API/Team/Feature/Profunctor.hs b/libs/wire-api/src/Wire/API/Team/Feature/Profunctor.hs new file mode 100644 index 00000000000..c6331cd6e3e --- /dev/null +++ b/libs/wire-api/src/Wire/API/Team/Feature/Profunctor.hs @@ -0,0 +1,53 @@ +-- 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 . + +-- | * schema-profunctor utilities for team features. +-- +-- This module contains utilities for building schemas for team features in +-- "Barbie" style, i.e. parameterised on a functor @f@. The functor can +-- normally be instantiated to either 'Identity' (for API values) or 'Maybe' +-- (for database values). +module Wire.API.Team.Feature.Profunctor where + +import Data.Default +import Data.Profunctor +import Data.Schema +import Imports + +-- | Parse an optional field by using its default when @f@ is 'Identity', and +-- leaving it as 'Nothing' when @f@ is 'Maybe'. +class OptWithDefault f where + fromOpt :: (Default a) => ObjectSchemaP SwaggerDoc a (Maybe a) -> ObjectSchema SwaggerDoc (f a) + +instance OptWithDefault Maybe where + fromOpt = maybe_ + +instance OptWithDefault Identity where + fromOpt = dimap runIdentity Identity . fmap (fromMaybe def) + +-- | This class enables non-standard JSON instances for the 'Identity' case of +-- this feature. In some cases, for backwards compatibility, we need to make a +-- field optional even in the 'Identity' case. A missing field gets parsed as +-- 'Nothing'. Whereas with the default instance, they would be rejected. +class NestedMaybe f where + nestedMaybeField :: Text -> ValueSchema SwaggerDoc a -> ObjectSchema SwaggerDoc (f (Maybe a)) + +instance NestedMaybe Maybe where + nestedMaybeField name sch = maybe_ (optField name (nullable sch)) + +instance NestedMaybe Identity where + nestedMaybeField name sch = Identity <$> runIdentity .= maybe_ (optField name sch) diff --git a/libs/wire-api/src/Wire/API/Team/Feature/TH.hs b/libs/wire-api/src/Wire/API/Team/Feature/TH.hs index bea09ca7802..6bf03852da6 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature/TH.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature/TH.hs @@ -1,3 +1,19 @@ +-- 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 TemplateHaskell #-} module Wire.API.Team.Feature.TH where diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 9af7d635c42..11751f85468 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -218,6 +218,7 @@ library Wire.API.Team.Conversation Wire.API.Team.Export Wire.API.Team.Feature + Wire.API.Team.Feature.Profunctor Wire.API.Team.Feature.TH Wire.API.Team.HardTruncationLimit Wire.API.Team.Invitation @@ -316,6 +317,7 @@ library , openapi3 , pem >=0.2 , polysemy + , profunctors , proto-lens , protobuf >=0.2 , QuickCheck >=2.14 diff --git a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs index 2695a948f46..d23f7a999ca 100644 --- a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs +++ b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs @@ -207,7 +207,8 @@ writeFeatures { status = conference_calling_status, lockStatus = conference_calling, config = - DbConfig . schemaToJSON . ConferenceCallingConfig <$> conference_calling_one_to_one + Just . DbConfig . schemaToJSON $ + ConferenceCallingConfig @Covered conference_calling_one_to_one } writeFeature @DigitalSignaturesConfig team_id $ From 65d3f94dce9b1d142f9a0a7b6e33382b0b356186 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 20 Feb 2025 09:31:02 +0100 Subject: [PATCH 13/25] Convert EnforceFileDownload to a barbie feature --- libs/wire-api/src/Wire/API/Team/Feature.hs | 34 ++++++++++++++----- .../src/V4_MigrateToDynamicFeatures.hs | 7 ++-- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 0e4728a8985..093ae96c4b9 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -78,7 +78,8 @@ module Wire.API.Team.Feature MlsE2EIdConfig, MlsMigrationConfigB (..), MlsMigrationConfig, - EnforceFileDownloadLocationConfig (..), + EnforceFileDownloadLocationConfigB (..), + EnforceFileDownloadLocationConfig, LimitedEventFanoutConfig (..), DomainRegistrationConfig (..), Features, @@ -1221,21 +1222,38 @@ instance IsFeatureConfig MlsMigrationConfig where ---------------------------------------------------------------------- -- EnforceFileDownloadLocationConfig -data EnforceFileDownloadLocationConfig = EnforceFileDownloadLocationConfig - { enforcedDownloadLocation :: Maybe Text +data EnforceFileDownloadLocationConfigB t f = EnforceFileDownloadLocationConfig + { enforcedDownloadLocation :: Wear t f (Maybe Text) } - deriving (Eq, Show, Generic, GSOP.Generic) - deriving (RenderableSymbol) via (RenderableTypeName EnforceFileDownloadLocationConfig) - deriving (Default, ParseDbFeature) via (SimpleFeature EnforceFileDownloadLocationConfig) + deriving (BareB, Generic) + +deriving instance FunctorB (EnforceFileDownloadLocationConfigB Covered) + +deriving instance ApplicativeB (EnforceFileDownloadLocationConfigB Covered) + +type EnforceFileDownloadLocationConfig = EnforceFileDownloadLocationConfigB Bare Identity + +deriving instance (Eq EnforceFileDownloadLocationConfig) + +deriving instance (Show EnforceFileDownloadLocationConfig) + +deriving via (RenderableTypeName EnforceFileDownloadLocationConfig) instance (RenderableSymbol EnforceFileDownloadLocationConfig) + +deriving via (BarbieFeature EnforceFileDownloadLocationConfigB) instance (ToSchema EnforceFileDownloadLocationConfig) + +deriving via (BarbieFeature EnforceFileDownloadLocationConfigB) instance (ParseDbFeature EnforceFileDownloadLocationConfig) + +instance Default EnforceFileDownloadLocationConfig where + def = EnforceFileDownloadLocationConfig Nothing instance Arbitrary EnforceFileDownloadLocationConfig where arbitrary = EnforceFileDownloadLocationConfig . fmap (T.pack . getPrintableString) <$> arbitrary -instance ToSchema EnforceFileDownloadLocationConfig where +instance (NestedMaybe f) => ToSchema (EnforceFileDownloadLocationConfigB Covered f) where schema = object "EnforceFileDownloadLocation" $ EnforceFileDownloadLocationConfig - <$> enforcedDownloadLocation .= maybe_ (optField "enforcedDownloadLocation" schema) + <$> enforcedDownloadLocation .= nestedMaybeField "enforcedDownloadLocation" (unnamed schema) instance Default (LockableFeature EnforceFileDownloadLocationConfig) where def = defLockedFeature diff --git a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs index d23f7a999ca..dedc372f727 100644 --- a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs +++ b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs @@ -225,8 +225,11 @@ writeFeatures { status = enforce_file_download_location_status, lockStatus = enforce_file_download_location_lock_status, config = - Just . DbConfig . schemaToJSON . EnforceFileDownloadLocationConfig $ - enforce_file_download_location + Just . DbConfig . schemaToJSON . EnforceFileDownloadLocationConfig @Covered $ + case enforce_file_download_location of + Nothing -> Nothing + Just "" -> Just Nothing + Just loc -> Just (Just loc) } writeFeature @ExposeInvitationURLsToTeamAdminConfig team_id $ From 5bcbfe6974b42aa075629fd53f8c2f40d7d03ab2 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 20 Feb 2025 09:36:04 +0100 Subject: [PATCH 14/25] Convert SelfDeletingMessages to a barbie feature --- libs/wire-api/src/Wire/API/Team/Feature.hs | 45 ++++++++++--------- .../src/V4_MigrateToDynamicFeatures.hs | 6 +-- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 093ae96c4b9..3c051515844 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -57,7 +57,8 @@ module Wire.API.Team.Feature LegalholdConfig (..), SSOConfig (..), SearchVisibilityAvailableConfig (..), - SelfDeletingMessagesConfig (..), + SelfDeletingMessagesConfigB (..), + SelfDeletingMessagesConfig, ValidateSAMLEmailsConfig (..), DigitalSignaturesConfig (..), ConferenceCallingConfigB (..), @@ -949,23 +950,37 @@ instance ToSchema FileSharingConfig where ---------------------------------------------------------------------- -- SelfDeletingMessagesConfig -data SelfDeletingMessagesConfig = SelfDeletingMessagesConfig - { sdmEnforcedTimeoutSeconds :: Int32 +data SelfDeletingMessagesConfigB t f = SelfDeletingMessagesConfig + { sdmEnforcedTimeoutSeconds :: Wear t f Int32 } - deriving (Eq, Show, Generic, GSOP.Generic) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema SelfDeletingMessagesConfig) - deriving (Arbitrary) via (GenericUniform SelfDeletingMessagesConfig) - deriving (RenderableSymbol) via (RenderableTypeName SelfDeletingMessagesConfig) - deriving (ParseDbFeature) via (SimpleFeature SelfDeletingMessagesConfig) + deriving (BareB, Generic) + +instance FunctorB (SelfDeletingMessagesConfigB Covered) + +instance ApplicativeB (SelfDeletingMessagesConfigB Covered) + +type SelfDeletingMessagesConfig = SelfDeletingMessagesConfigB Bare Identity + +deriving instance (Eq SelfDeletingMessagesConfig) + +deriving instance (Show SelfDeletingMessagesConfig) + +deriving via (GenericUniform SelfDeletingMessagesConfig) instance (Arbitrary SelfDeletingMessagesConfig) + +deriving via (RenderableTypeName SelfDeletingMessagesConfig) instance (RenderableSymbol SelfDeletingMessagesConfig) + +deriving via (BarbieFeature SelfDeletingMessagesConfigB) instance (ParseDbFeature SelfDeletingMessagesConfig) + +deriving via (BarbieFeature SelfDeletingMessagesConfigB) instance (ToSchema SelfDeletingMessagesConfig) instance Default SelfDeletingMessagesConfig where def = SelfDeletingMessagesConfig 0 -instance ToSchema SelfDeletingMessagesConfig where +instance (FieldFunctor SwaggerDoc f) => ToSchema (SelfDeletingMessagesConfigB Covered f) where schema = object "SelfDeletingMessagesConfig" $ SelfDeletingMessagesConfig - <$> sdmEnforcedTimeoutSeconds .= field "enforcedTimeoutSeconds" schema + <$> sdmEnforcedTimeoutSeconds .= extractF (fieldF "enforcedTimeoutSeconds" schema) instance Default (LockableFeature SelfDeletingMessagesConfig) where def = defUnlockedFeature @@ -1505,16 +1520,6 @@ instance (GSOP.IsProductType cfg '[]) => ParseDbFeature (TrivialFeature cfg) whe instance (GSOP.IsProductType cfg '[]) => Default (TrivialFeature cfg) where def = TrivialFeature (GSOP.productTypeTo Nil) -newtype SimpleFeature cfg = SimpleFeature cfg - -instance (GSOP.IsWrappedType cfg a, ToSchema cfg) => ParseDbFeature (SimpleFeature cfg) where - parseDbConfig (DbConfig v) = do - config <- schemaParseJSON v - pure . const $ SimpleFeature config - -instance (GSOP.IsWrappedType cfg a, Default a) => Default (SimpleFeature cfg) where - def = SimpleFeature (GSOP.wrappedTypeTo def) - newtype BarbieFeature b = BarbieFeature {unBarbieFeature :: b Bare Identity} instance diff --git a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs index dedc372f727..d5f7a94a8e1 100644 --- a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs +++ b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs @@ -317,10 +317,10 @@ writeFeatures { status = self_deleting_messages_status, lockStatus = self_deleting_messages_lock_status, config = - DbConfig + Just + . DbConfig . schemaToJSON - . SelfDeletingMessagesConfig - <$> self_deleting_messages_ttl + $ SelfDeletingMessagesConfig @Covered self_deleting_messages_ttl } writeFeature @SndFactorPasswordChallengeConfig team_id $ From c35275c5fa413cbb84973f0e379de24827f9c354 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 20 Feb 2025 15:32:25 +0100 Subject: [PATCH 15/25] Avoid migrating null features --- libs/wire-api/src/Wire/API/Team/Feature.hs | 14 ++ services/galley/galley.cabal | 1 + .../src/V4_MigrateToDynamicFeatures.hs | 195 +++++++++++------- 3 files changed, 136 insertions(+), 74 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 3c051515844..fc60a7bec4c 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -760,6 +760,8 @@ deriving instance FunctorB (ConferenceCallingConfigB Covered) deriving instance ApplicativeB (ConferenceCallingConfigB Covered) +deriving instance TraversableB (ConferenceCallingConfigB Covered) + type ConferenceCallingConfig = ConferenceCallingConfigB Bare Identity deriving instance (Eq ConferenceCallingConfig) @@ -883,6 +885,8 @@ instance FunctorB (AppLockConfigB Covered) instance ApplicativeB (AppLockConfigB Covered) +instance TraversableB (AppLockConfigB Covered) + type AppLockConfig = AppLockConfigB Bare Identity deriving instance Eq AppLockConfig @@ -959,6 +963,8 @@ instance FunctorB (SelfDeletingMessagesConfigB Covered) instance ApplicativeB (SelfDeletingMessagesConfigB Covered) +instance TraversableB (SelfDeletingMessagesConfigB Covered) + type SelfDeletingMessagesConfig = SelfDeletingMessagesConfigB Bare Identity deriving instance (Eq SelfDeletingMessagesConfig) @@ -1006,6 +1012,8 @@ deriving instance FunctorB (MLSConfigB Covered) deriving instance ApplicativeB (MLSConfigB Covered) +deriving instance TraversableB (MLSConfigB Covered) + type MLSConfig = MLSConfigB Bare Identity deriving instance Eq MLSConfig @@ -1118,6 +1126,8 @@ deriving instance FunctorB (MlsE2EIdConfigB Covered) deriving instance ApplicativeB (MlsE2EIdConfigB Covered) +deriving instance TraversableB (MlsE2EIdConfigB Covered) + type MlsE2EIdConfig = MlsE2EIdConfigB Bare Identity deriving via (RenderableTypeName MlsE2EIdConfig) instance (RenderableSymbol MlsE2EIdConfig) @@ -1194,6 +1204,8 @@ deriving instance FunctorB (MlsMigrationConfigB Covered) deriving instance ApplicativeB (MlsMigrationConfigB Covered) +deriving instance TraversableB (MlsMigrationConfigB Covered) + type MlsMigrationConfig = MlsMigrationConfigB Bare Identity deriving instance Eq MlsMigrationConfig @@ -1246,6 +1258,8 @@ deriving instance FunctorB (EnforceFileDownloadLocationConfigB Covered) deriving instance ApplicativeB (EnforceFileDownloadLocationConfigB Covered) +deriving instance TraversableB (EnforceFileDownloadLocationConfigB Covered) + type EnforceFileDownloadLocationConfig = EnforceFileDownloadLocationConfigB Bare Identity deriving instance (Eq EnforceFileDownloadLocationConfig) diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index df7cb97a3aa..c141e005f06 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -573,6 +573,7 @@ executable galley-migrate-data build-depends: , barbies , base + , bytestring-conversion , cassandra-util , conduit , containers diff --git a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs index d5f7a94a8e1..6e177aef96f 100644 --- a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs +++ b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs @@ -3,10 +3,12 @@ module V4_MigrateToDynamicFeatures where +import Barbies import Barbies.Bare import Cassandra import Cassandra qualified as C import Conduit +import Data.ByteString.Conversion import Data.Conduit.List qualified as C import Data.Default import Data.Id @@ -16,6 +18,7 @@ import Data.Time.Clock import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) import Galley.DataMigration.Types import Imports +import System.Logger.Class qualified as Log import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.Team.Feature @@ -142,7 +145,7 @@ rowQuery = \ from team_features" writeFeatures :: - (MonadClient m) => + (MonadClient m, Log.MonadLogger m) => FeatureRow -> m () writeFeatures @@ -195,145 +198,147 @@ writeFeatures sso_status, validate_saml_emails ) = do - writeFeature @AppLockConfig team_id $ - (def :: LockableFeaturePatch DbConfig) + writeFeatureB team_id $ + (def :: LockableFeaturePatch AppLockConfig) { status = app_lock_status, - config = - Just . DbConfig $ - schemaToJSON (AppLockConfig @Covered app_lock_enforce app_lock_inactivity_timeout_secs) + config = Just $ AppLockConfig @Covered app_lock_enforce app_lock_inactivity_timeout_secs } - writeFeature @ConferenceCallingConfig team_id $ + writeFeatureB team_id $ LockableFeaturePatch { status = conference_calling_status, lockStatus = conference_calling, config = - Just . DbConfig . schemaToJSON $ - ConferenceCallingConfig @Covered conference_calling_one_to_one + Just $ ConferenceCallingConfig @Covered conference_calling_one_to_one } - writeFeature @DigitalSignaturesConfig team_id $ - (def :: LockableFeaturePatch DbConfig) {status = digital_signatures} + writeFeature team_id $ + (def :: LockableFeaturePatch DigitalSignaturesConfig) {status = digital_signatures} - writeFeature @DomainRegistrationConfig team_id $ - (def :: LockableFeaturePatch DbConfig) + writeFeature team_id $ + (def :: LockableFeaturePatch DomainRegistrationConfig) { status = domain_registration_status, lockStatus = domain_registration_lock_status } - writeFeature @EnforceFileDownloadLocationConfig team_id $ + writeFeatureB team_id $ LockableFeaturePatch { status = enforce_file_download_location_status, lockStatus = enforce_file_download_location_lock_status, config = - Just . DbConfig . schemaToJSON . EnforceFileDownloadLocationConfig @Covered $ - case enforce_file_download_location of - Nothing -> Nothing - Just "" -> Just Nothing - Just loc -> Just (Just loc) + Just $ + EnforceFileDownloadLocationConfig @Covered $ + case enforce_file_download_location of + Nothing -> Nothing + Just "" -> Just Nothing + Just loc -> Just (Just loc) } - writeFeature @ExposeInvitationURLsToTeamAdminConfig team_id $ - (def :: LockableFeaturePatch DbConfig) {status = expose_invitation_urls_to_team_admin} + writeFeature team_id $ + (def :: LockableFeaturePatch ExposeInvitationURLsToTeamAdminConfig) + { status = expose_invitation_urls_to_team_admin + } - writeFeature @FileSharingConfig team_id $ - (def :: LockableFeaturePatch DbConfig) + writeFeature team_id $ + (def :: LockableFeaturePatch FileSharingConfig) { status = file_sharing, lockStatus = file_sharing_lock_status } - writeFeature @GuestLinksConfig team_id $ - (def :: LockableFeaturePatch DbConfig) + writeFeature team_id $ + (def :: LockableFeaturePatch GuestLinksConfig) { status = guest_links_status, lockStatus = guest_links_lock_status } - writeFeature @LegalholdConfig team_id $ - (def :: LockableFeaturePatch DbConfig) {status = legalhold_status} - writeFeature @LimitedEventFanoutConfig team_id $ - (def :: LockableFeaturePatch DbConfig) {status = limited_event_fanout_status} + writeFeature team_id $ + (def :: LockableFeaturePatch LegalholdConfig) + { status = legalhold_status + } - writeFeature @MLSConfig team_id $ + writeFeature team_id $ + (def :: LockableFeaturePatch LimitedEventFanoutConfig) + { status = limited_event_fanout_status + } + + writeFeatureB team_id $ LockableFeaturePatch { status = mls_status, lockStatus = mls_lock_status, config = - Just . DbConfig $ - schemaToJSON - ( MLSConfig @Covered - (fmap C.fromSet mls_protocol_toggle_users) - mls_default_protocol - (fmap C.fromSet mls_allowed_ciphersuites) - mls_default_ciphersuite - (fmap C.fromSet mls_supported_protocols) - ) + Just $ + ( MLSConfig @Covered + (fmap C.fromSet mls_protocol_toggle_users) + mls_default_protocol + (fmap C.fromSet mls_allowed_ciphersuites) + mls_default_ciphersuite + (fmap C.fromSet mls_supported_protocols) + ) } - writeFeature @MlsE2EIdConfig team_id $ + writeFeatureB team_id $ LockableFeaturePatch { status = mls_e2eid_status, lockStatus = mls_e2eid_lock_status, config = - Just . DbConfig $ - schemaToJSON - ( MlsE2EIdConfig @Covered - (fmap fromIntegral mls_e2eid_grace_period) - (Alt mls_e2eid_acme_discovery_url) - (Alt mls_e2eid_crl_proxy) - (maybe def UseProxyOnMobile mls_e2eid_use_proxy_on_mobile) - ) + Just $ + ( MlsE2EIdConfig @Covered + (fmap fromIntegral mls_e2eid_grace_period) + (Alt mls_e2eid_acme_discovery_url) + (Alt mls_e2eid_crl_proxy) + (maybe def UseProxyOnMobile mls_e2eid_use_proxy_on_mobile) + ) } - writeFeature @MlsMigrationConfig team_id $ + writeFeatureB team_id $ LockableFeaturePatch { status = mls_migration_status, lockStatus = mls_migration_lock_status, config = - Just . DbConfig $ - schemaToJSON - ( MlsMigrationConfig @Covered - (fmap unOptionalUTCTime mls_migration_start_time) - (fmap unOptionalUTCTime mls_migration_finalise_regardless_after) - ) + Just $ + ( MlsMigrationConfig @Covered + (fmap unOptionalUTCTime mls_migration_start_time) + (fmap unOptionalUTCTime mls_migration_finalise_regardless_after) + ) } - writeFeature @OutlookCalIntegrationConfig team_id $ - (def :: LockableFeaturePatch DbConfig) + writeFeature team_id $ + (def :: LockableFeaturePatch OutlookCalIntegrationConfig) { status = outlook_cal_integration_status, lockStatus = outlook_cal_integration_lock_status } - writeFeature @SearchVisibilityInboundConfig team_id $ - (def :: LockableFeaturePatch DbConfig) + writeFeature team_id $ + (def :: LockableFeaturePatch SearchVisibilityInboundConfig) { status = search_visibility_inbound_status } - writeFeature @SearchVisibilityAvailableConfig team_id $ - (def :: LockableFeaturePatch DbConfig) + writeFeature team_id $ + (def :: LockableFeaturePatch SearchVisibilityAvailableConfig) { status = search_visibility_status } - writeFeature @SelfDeletingMessagesConfig team_id $ + writeFeatureB team_id $ LockableFeaturePatch { status = self_deleting_messages_status, lockStatus = self_deleting_messages_lock_status, config = - Just - . DbConfig - . schemaToJSON - $ SelfDeletingMessagesConfig @Covered self_deleting_messages_ttl + Just $ + SelfDeletingMessagesConfig @Covered self_deleting_messages_ttl } - writeFeature @SndFactorPasswordChallengeConfig team_id $ - (def :: LockableFeaturePatch DbConfig) + writeFeature team_id $ + (def :: LockableFeaturePatch SndFactorPasswordChallengeConfig) { status = snd_factor_password_challenge_status, lockStatus = snd_factor_password_challenge_lock_status } - writeFeature @SSOConfig team_id $ - (def :: LockableFeaturePatch DbConfig) {status = sso_status} + writeFeature team_id $ + (def :: LockableFeaturePatch SSOConfig) {status = sso_status} - writeFeature @ValidateSAMLEmailsConfig team_id $ - (def :: LockableFeaturePatch DbConfig) {status = validate_saml_emails} + writeFeature team_id $ + (def :: LockableFeaturePatch ValidateSAMLEmailsConfig) + { status = validate_saml_emails + } ---------------------------------------------------------------------------- @@ -378,11 +383,42 @@ instance Cql OptionalUTCTime where writeFeature :: forall cfg m. - (IsFeatureConfig cfg, MonadClient m) => + (IsFeatureConfig cfg, MonadClient m, Log.MonadLogger m) => + TeamId -> + LockableFeaturePatch cfg -> + m () +writeFeature tid feat = + writeDbFeature @cfg + tid + feat {config = fmap (DbConfig . schemaToJSON) feat.config} + +writeFeatureB :: + forall b cfg m. + ( cfg ~ b Bare Identity, + ToSchema (b Covered Maybe), + TraversableB (b Covered), + IsFeatureConfig cfg, + MonadClient m, + Log.MonadLogger m + ) => + TeamId -> + LockableFeaturePatch (b Covered Maybe) -> + m () +writeFeatureB tid feat = do + let dbConfig = feat.config >>= serialiseBarbieConfig + writeDbFeature @cfg tid feat {config = dbConfig} + +writeDbFeature :: + forall cfg m. + (IsFeatureConfig cfg, MonadClient m, Log.MonadLogger m) => TeamId -> LockableFeaturePatch DbConfig -> m () -writeFeature tid feat = do +writeDbFeature tid feat = do + Log.info $ + Log.msg ("writing feature" :: ByteString) + . Log.field "team" (toByteString' tid) + . Log.field "feature" (featureName @cfg) let q :: PrepQuery W (Maybe FeatureStatus, Maybe LockStatus, Maybe DbConfig, TeamId, Text) () q = "update team_features_dyn set status = ?, lock_status = ?, config = ? where team = ? and feature = ?" retry x5 $ @@ -397,3 +433,14 @@ writeFeature tid feat = do featureName @cfg ) ) + +serialiseBarbieConfig :: + ( TraversableB (b Covered), + ToSchema (b Covered Maybe) + ) => + b Covered Maybe -> + Maybe DbConfig +serialiseBarbieConfig cfg = do + -- ensure at least one field is set + void $ getAlt (bfoldMap (Alt . void) cfg) + pure . DbConfig $ schemaToJSON cfg From 84d4d19ab3a15941c6e48613753a130bd305820b Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 20 Feb 2025 15:45:59 +0100 Subject: [PATCH 16/25] Update migration state during migration --- .../src/Galley/DataMigration/Types.hs | 7 +- .../src/V4_MigrateToDynamicFeatures.hs | 303 +++++++++--------- 2 files changed, 164 insertions(+), 146 deletions(-) diff --git a/services/galley/migrate-data/src/Galley/DataMigration/Types.hs b/services/galley/migrate-data/src/Galley/DataMigration/Types.hs index 489ac309271..4a95dbc9ac7 100644 --- a/services/galley/migrate-data/src/Galley/DataMigration/Types.hs +++ b/services/galley/migrate-data/src/Galley/DataMigration/Types.hs @@ -20,7 +20,7 @@ module Galley.DataMigration.Types where import Cassandra qualified as C -import Control.Monad.Catch (MonadThrow) +import Control.Monad.Catch (MonadCatch, MonadThrow) import Imports import Numeric.Natural (Natural) import System.Logger qualified as Logger @@ -41,9 +41,10 @@ newtype MigrationActionT m a = MigrationActionT {unMigrationAction :: ReaderT En Applicative, Monad, MonadIO, - MonadThrow, MonadReader Env, - MonadUnliftIO + MonadUnliftIO, + MonadThrow, + MonadCatch ) instance MonadTrans MigrationActionT where diff --git a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs index 6e177aef96f..4d3c6f43519 100644 --- a/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs +++ b/services/galley/migrate-data/src/V4_MigrateToDynamicFeatures.hs @@ -8,6 +8,7 @@ import Barbies.Bare import Cassandra import Cassandra qualified as C import Conduit +import Control.Monad.Catch import Data.ByteString.Conversion import Data.Conduit.List qualified as C import Data.Default @@ -145,7 +146,7 @@ rowQuery = \ from team_features" writeFeatures :: - (MonadClient m, Log.MonadLogger m) => + (MonadClient m, Log.MonadLogger m, MonadCatch m) => FeatureRow -> m () writeFeatures @@ -197,148 +198,158 @@ writeFeatures snd_factor_password_challenge_status, sso_status, validate_saml_emails - ) = do - writeFeatureB team_id $ - (def :: LockableFeaturePatch AppLockConfig) - { status = app_lock_status, - config = Just $ AppLockConfig @Covered app_lock_enforce app_lock_inactivity_timeout_secs - } - writeFeatureB team_id $ - LockableFeaturePatch - { status = conference_calling_status, - lockStatus = conference_calling, - config = - Just $ ConferenceCallingConfig @Covered conference_calling_one_to_one - } - - writeFeature team_id $ - (def :: LockableFeaturePatch DigitalSignaturesConfig) {status = digital_signatures} - - writeFeature team_id $ - (def :: LockableFeaturePatch DomainRegistrationConfig) - { status = domain_registration_status, - lockStatus = domain_registration_lock_status - } - - writeFeatureB team_id $ - LockableFeaturePatch - { status = enforce_file_download_location_status, - lockStatus = enforce_file_download_location_lock_status, - config = - Just $ - EnforceFileDownloadLocationConfig @Covered $ - case enforce_file_download_location of - Nothing -> Nothing - Just "" -> Just Nothing - Just loc -> Just (Just loc) - } - - writeFeature team_id $ - (def :: LockableFeaturePatch ExposeInvitationURLsToTeamAdminConfig) - { status = expose_invitation_urls_to_team_admin - } - - writeFeature team_id $ - (def :: LockableFeaturePatch FileSharingConfig) - { status = file_sharing, - lockStatus = file_sharing_lock_status - } - - writeFeature team_id $ - (def :: LockableFeaturePatch GuestLinksConfig) - { status = guest_links_status, - lockStatus = guest_links_lock_status - } - - writeFeature team_id $ - (def :: LockableFeaturePatch LegalholdConfig) - { status = legalhold_status - } - - writeFeature team_id $ - (def :: LockableFeaturePatch LimitedEventFanoutConfig) - { status = limited_event_fanout_status - } - - writeFeatureB team_id $ - LockableFeaturePatch - { status = mls_status, - lockStatus = mls_lock_status, - config = - Just $ - ( MLSConfig @Covered - (fmap C.fromSet mls_protocol_toggle_users) - mls_default_protocol - (fmap C.fromSet mls_allowed_ciphersuites) - mls_default_ciphersuite - (fmap C.fromSet mls_supported_protocols) - ) - } - - writeFeatureB team_id $ - LockableFeaturePatch - { status = mls_e2eid_status, - lockStatus = mls_e2eid_lock_status, - config = - Just $ - ( MlsE2EIdConfig @Covered - (fmap fromIntegral mls_e2eid_grace_period) - (Alt mls_e2eid_acme_discovery_url) - (Alt mls_e2eid_crl_proxy) - (maybe def UseProxyOnMobile mls_e2eid_use_proxy_on_mobile) - ) - } - - writeFeatureB team_id $ - LockableFeaturePatch - { status = mls_migration_status, - lockStatus = mls_migration_lock_status, - config = - Just $ - ( MlsMigrationConfig @Covered - (fmap unOptionalUTCTime mls_migration_start_time) - (fmap unOptionalUTCTime mls_migration_finalise_regardless_after) - ) - } - - writeFeature team_id $ - (def :: LockableFeaturePatch OutlookCalIntegrationConfig) - { status = outlook_cal_integration_status, - lockStatus = outlook_cal_integration_lock_status - } - - writeFeature team_id $ - (def :: LockableFeaturePatch SearchVisibilityInboundConfig) - { status = search_visibility_inbound_status - } - - writeFeature team_id $ - (def :: LockableFeaturePatch SearchVisibilityAvailableConfig) - { status = search_visibility_status - } - - writeFeatureB team_id $ - LockableFeaturePatch - { status = self_deleting_messages_status, - lockStatus = self_deleting_messages_lock_status, - config = - Just $ - SelfDeletingMessagesConfig @Covered self_deleting_messages_ttl - } - - writeFeature team_id $ - (def :: LockableFeaturePatch SndFactorPasswordChallengeConfig) - { status = snd_factor_password_challenge_status, - lockStatus = snd_factor_password_challenge_lock_status - } - - writeFeature team_id $ - (def :: LockableFeaturePatch SSOConfig) {status = sso_status} - - writeFeature team_id $ - (def :: LockableFeaturePatch ValidateSAMLEmailsConfig) - { status = validate_saml_emails - } + ) = + onException + ( do + -- set team features to read-only + setMigrationState team_id MigrationInProgress + + writeFeatureB team_id $ + (def :: LockableFeaturePatch AppLockConfig) + { status = app_lock_status, + config = Just $ AppLockConfig @Covered app_lock_enforce app_lock_inactivity_timeout_secs + } + writeFeatureB team_id $ + LockableFeaturePatch + { status = conference_calling_status, + lockStatus = conference_calling, + config = + Just $ ConferenceCallingConfig @Covered conference_calling_one_to_one + } + + writeFeature team_id $ + (def :: LockableFeaturePatch DigitalSignaturesConfig) {status = digital_signatures} + + writeFeature team_id $ + (def :: LockableFeaturePatch DomainRegistrationConfig) + { status = domain_registration_status, + lockStatus = domain_registration_lock_status + } + + writeFeatureB team_id $ + LockableFeaturePatch + { status = enforce_file_download_location_status, + lockStatus = enforce_file_download_location_lock_status, + config = + Just $ + EnforceFileDownloadLocationConfig @Covered $ + case enforce_file_download_location of + Nothing -> Nothing + Just "" -> Just Nothing + Just loc -> Just (Just loc) + } + + writeFeature team_id $ + (def :: LockableFeaturePatch ExposeInvitationURLsToTeamAdminConfig) + { status = expose_invitation_urls_to_team_admin + } + + writeFeature team_id $ + (def :: LockableFeaturePatch FileSharingConfig) + { status = file_sharing, + lockStatus = file_sharing_lock_status + } + + writeFeature team_id $ + (def :: LockableFeaturePatch GuestLinksConfig) + { status = guest_links_status, + lockStatus = guest_links_lock_status + } + + writeFeature team_id $ + (def :: LockableFeaturePatch LegalholdConfig) + { status = legalhold_status + } + + writeFeature team_id $ + (def :: LockableFeaturePatch LimitedEventFanoutConfig) + { status = limited_event_fanout_status + } + + writeFeatureB team_id $ + LockableFeaturePatch + { status = mls_status, + lockStatus = mls_lock_status, + config = + Just $ + ( MLSConfig @Covered + (fmap C.fromSet mls_protocol_toggle_users) + mls_default_protocol + (fmap C.fromSet mls_allowed_ciphersuites) + mls_default_ciphersuite + (fmap C.fromSet mls_supported_protocols) + ) + } + + writeFeatureB team_id $ + LockableFeaturePatch + { status = mls_e2eid_status, + lockStatus = mls_e2eid_lock_status, + config = + Just $ + ( MlsE2EIdConfig @Covered + (fmap fromIntegral mls_e2eid_grace_period) + (Alt mls_e2eid_acme_discovery_url) + (Alt mls_e2eid_crl_proxy) + (maybe def UseProxyOnMobile mls_e2eid_use_proxy_on_mobile) + ) + } + + writeFeatureB team_id $ + LockableFeaturePatch + { status = mls_migration_status, + lockStatus = mls_migration_lock_status, + config = + Just $ + ( MlsMigrationConfig @Covered + (fmap unOptionalUTCTime mls_migration_start_time) + (fmap unOptionalUTCTime mls_migration_finalise_regardless_after) + ) + } + + writeFeature team_id $ + (def :: LockableFeaturePatch OutlookCalIntegrationConfig) + { status = outlook_cal_integration_status, + lockStatus = outlook_cal_integration_lock_status + } + + writeFeature team_id $ + (def :: LockableFeaturePatch SearchVisibilityInboundConfig) + { status = search_visibility_inbound_status + } + + writeFeature team_id $ + (def :: LockableFeaturePatch SearchVisibilityAvailableConfig) + { status = search_visibility_status + } + + writeFeatureB team_id $ + LockableFeaturePatch + { status = self_deleting_messages_status, + lockStatus = self_deleting_messages_lock_status, + config = + Just $ + SelfDeletingMessagesConfig @Covered self_deleting_messages_ttl + } + + writeFeature team_id $ + (def :: LockableFeaturePatch SndFactorPasswordChallengeConfig) + { status = snd_factor_password_challenge_status, + lockStatus = snd_factor_password_challenge_lock_status + } + + writeFeature team_id $ + (def :: LockableFeaturePatch SSOConfig) {status = sso_status} + + writeFeature team_id $ + (def :: LockableFeaturePatch ValidateSAMLEmailsConfig) + { status = validate_saml_emails + } + + -- set migration state to completed + setMigrationState team_id MigrationCompleted + ) + (setMigrationState team_id MigrationNotStarted) ---------------------------------------------------------------------------- @@ -444,3 +455,9 @@ serialiseBarbieConfig cfg = do -- ensure at least one field is set void $ getAlt (bfoldMap (Alt . void) cfg) pure . DbConfig $ schemaToJSON cfg + +setMigrationState :: (MonadClient m) => TeamId -> TeamFeatureMigrationState -> m () +setMigrationState tid state = do + let q :: PrepQuery W (TeamFeatureMigrationState, TeamId) () + q = "update team_features set migration_state = ? where team_id = ?" + retry x5 $ write q (params LocalQuorum (state, tid)) From eb84909adc66d5c66843fa77b8becc177c1310fc Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 21 Feb 2025 08:51:34 +0100 Subject: [PATCH 17/25] Regenerate nix packages --- libs/wire-api/default.nix | 6 ++++++ services/galley/default.nix | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 9b6c2c3bd56..5e4e2cf25ac 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -10,6 +10,7 @@ , asn1-encoding , async , attoparsec +, barbies , base , base64-bytestring , binary @@ -23,6 +24,7 @@ , cereal , comonad , conduit +, constraints , containers , cookie , crypton @@ -68,6 +70,7 @@ , pem , polysemy , process +, profunctors , proto-lens , protobuf , QuickCheck @@ -123,6 +126,7 @@ mkDerivation { aeson asn1-encoding attoparsec + barbies base base64-bytestring binary @@ -135,6 +139,7 @@ mkDerivation { cereal comonad conduit + constraints containers cookie crypton @@ -174,6 +179,7 @@ mkDerivation { openapi3 pem polysemy + profunctors proto-lens protobuf QuickCheck diff --git a/services/galley/default.nix b/services/galley/default.nix index 7cec256aa46..bb7b1dc1b6e 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -11,6 +11,7 @@ , asn1-encoding , asn1-types , async +, barbies , base , base64-bytestring , bilge @@ -24,6 +25,7 @@ , cereal , comonad , conduit +, constraints , containers , cookie , crypton @@ -78,6 +80,7 @@ , resourcet , retry , safe-exceptions +, schema-profunctor , servant , servant-client , servant-client-core @@ -148,6 +151,7 @@ mkDerivation { cassandra-util cassava comonad + constraints containers crypton crypton-x509 @@ -221,6 +225,7 @@ mkDerivation { aeson aeson-qq async + barbies base base64-bytestring bilge @@ -268,6 +273,7 @@ mkDerivation { quickcheck-instances random retry + schema-profunctor servant-client servant-client-core servant-server From ca47e3f798b29c89b5dbdfb2be62ff7245530cda Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 21 Feb 2025 09:09:01 +0100 Subject: [PATCH 18/25] Add CHANGELOG entries --- changelog.d/0-release-notes/simplify-feature-table | 1 + changelog.d/5-internal/simplify-feature-table | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/0-release-notes/simplify-feature-table create mode 100644 changelog.d/5-internal/simplify-feature-table diff --git a/changelog.d/0-release-notes/simplify-feature-table b/changelog.d/0-release-notes/simplify-feature-table new file mode 100644 index 00000000000..d7f9579e55f --- /dev/null +++ b/changelog.d/0-release-notes/simplify-feature-table @@ -0,0 +1 @@ +This release introduces a new data storage format for team features and a corresponding migration. While the migration is running, team features are going to operate in read-only mode for the team that is currently being migrated. After migration, the new storage is going to be used. No special action should be required on the part of instance operators. diff --git a/changelog.d/5-internal/simplify-feature-table b/changelog.d/5-internal/simplify-feature-table new file mode 100644 index 00000000000..57d973401a5 --- /dev/null +++ b/changelog.d/5-internal/simplify-feature-table @@ -0,0 +1 @@ +Introduce a new feature table in Cassandra: `team_features_dyn`. This table has a fixed number of fields, as opposed to the ever-growing collection of all the fields of all the features that we were using before. From 2bd005312cb578fec89cf2a716876013a24478cf Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 21 Feb 2025 09:13:29 +0100 Subject: [PATCH 19/25] Lint --- libs/wire-api/src/Wire/API/Team/Feature.hs | 1 - services/galley/src/Galley/Cassandra/MakeFeature.hs | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index fc60a7bec4c..d98551fad62 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -2,7 +2,6 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. diff --git a/services/galley/src/Galley/Cassandra/MakeFeature.hs b/services/galley/src/Galley/Cassandra/MakeFeature.hs index de915c32ca0..7f87388764e 100644 --- a/services/galley/src/Galley/Cassandra/MakeFeature.hs +++ b/services/galley/src/Galley/Cassandra/MakeFeature.hs @@ -339,9 +339,7 @@ instance MakeFeature MlsE2EIdConfig where acmeDiscoveryUrl = Alt acmeDiscoveryUrl <|> defCfg.acmeDiscoveryUrl, crlProxy = Alt crlProxy <|> defCfg.crlProxy, useProxyOnMobile = - fromMaybe - defCfg.useProxyOnMobile - (fmap UseProxyOnMobile useProxyOnMobile) + maybe defCfg.useProxyOnMobile UseProxyOnMobile useProxyOnMobile } ) From 7a1b3ecc0215cf7303111186cf427f92c0da8103 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 24 Feb 2025 09:00:00 +0100 Subject: [PATCH 20/25] Honour feature table selection in runFeatureTests --- integration/test/Test/FeatureFlags/Util.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index daa087bb7fc..38944d0f1c1 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -325,6 +325,7 @@ runFeatureTests domain access ft = do Just owner -> do tid <- owner %. "team" & asString pure (owner, tid) + updateMigrationState domain tid ft.table checkFeature ft.name owner tid defFeature -- lock the feature From 5fc4f36bf8a0554354238d3ac5e0232dc712cdd4 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 24 Feb 2025 15:21:52 +0100 Subject: [PATCH 21/25] Fix EnforceFileDownloadLocation parser --- libs/schema-profunctor/src/Data/Schema.hs | 78 ++++++++----------- libs/wire-api/src/Wire/API/Team/Feature.hs | 77 +++++++++++------- .../src/Wire/API/Team/Feature/Profunctor.hs | 26 ++++++- libs/wire-api/src/Wire/API/User/RichInfo.hs | 4 +- 4 files changed, 109 insertions(+), 76 deletions(-) diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index 36d5287a7ce..3bb534f0fb3 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -36,6 +36,7 @@ module Data.Schema schemaIn, schemaOut, HasDoc (..), + HasOpt (..), doc', HasSchemaRef (..), HasObject (..), @@ -52,15 +53,12 @@ module Data.Schema objectOver, jsonObject, jsonValue, - FieldFunctor (..), field, fieldWithDocModifier, - fieldOver, optField, + optField', optFieldWithDocModifier, - fieldF, - fieldOverF, - fieldWithDocModifierF, + fieldOver, array, set, nonEmptyArray, @@ -283,21 +281,6 @@ schemaIn (SchemaP _ (SchemaIn i) _) = i schemaOut :: SchemaP ss v m a b -> a -> Maybe m schemaOut (SchemaP _ _ (SchemaOut o)) = o -class (Functor f) => FieldFunctor doc f where - parseFieldF :: (A.Value -> A.Parser a) -> A.Object -> Text -> A.Parser (f a) - extractF :: (Monoid w) => SchemaP doc v w a b -> SchemaP doc v w (f a) b - mkDocF :: doc -> doc - -instance FieldFunctor doc Identity where - parseFieldF f obj key = Identity <$> A.explicitParseField f obj (Key.fromText key) - extractF = lmap runIdentity - mkDocF = id - -instance (HasOpt doc) => FieldFunctor doc Maybe where - parseFieldF f obj key = A.explicitParseFieldMaybe f obj (Key.fromText key) - extractF = maybe_ - mkDocF = mkOpt - -- | A schema for a one-field JSON object. field :: forall doc' doc a b. @@ -305,7 +288,7 @@ field :: Text -> SchemaP doc' A.Value A.Value a b -> SchemaP doc A.Object [A.Pair] a b -field = fieldOver id +field name = fieldOver id name -- | A schema for a JSON object with a single optional field. optField :: @@ -314,16 +297,24 @@ optField :: Text -> SchemaP doc' A.Value A.Value a b -> SchemaP doc A.Object [A.Pair] a (Maybe b) -optField = fieldF - --- | Generalization of 'optField' with 'FieldFunctor'. -fieldF :: - forall doc' doc f a b. - (HasField doc' doc, FieldFunctor doc f) => +optField = + fieldOverF + (\f obj key -> A.explicitParseFieldMaybe f obj (Key.fromText key)) + mkOpt + id + +-- | Same as 'optField', but null values are parsed as normal values rather than 'Nothing'. +optField' :: + forall doc doc' a b. + (HasOpt doc, HasField doc' doc) => Text -> SchemaP doc' A.Value A.Value a b -> - SchemaP doc A.Object [A.Pair] a (f b) -fieldF = fieldOverF id + SchemaP doc A.Object [A.Pair] a (Maybe b) +optField' = + fieldOverF + (\f obj key -> A.explicitParseFieldMaybe' f obj (Key.fromText key)) + mkOpt + id newtype Positive x y a = Positive {runPositive :: (a -> x) -> y} deriving (Functor) @@ -337,15 +328,17 @@ newtype Positive x y a = Positive {runPositive :: (a -> x) -> y} -- See 'bind' for use cases. fieldOverF :: forall f doc' doc v v' a b. - (HasField doc' doc, FieldFunctor doc f) => + (HasField doc' doc) => + (forall x. (A.Value -> A.Parser x) -> A.Object -> Text -> A.Parser (f x)) -> + (doc -> doc) -> Lens v v' A.Object A.Value -> Text -> SchemaP doc' v' A.Value a b -> SchemaP doc v [A.Pair] a (f b) -fieldOverF l name sch = SchemaP (SchemaDoc s) (SchemaIn r) (SchemaOut w) +fieldOverF pf mkDoc l name sch = SchemaP (SchemaDoc s) (SchemaIn r) (SchemaOut w) where parseField :: A.Object -> Positive (A.Parser b) (A.Parser (f b)) A.Value - parseField obj = Positive $ \k -> parseFieldF @doc k obj name + parseField obj = Positive $ \k -> pf k obj name r :: v -> A.Parser (f b) r obj = runPositive (l parseField obj) (schemaIn sch) @@ -354,7 +347,7 @@ fieldOverF l name sch = SchemaP (SchemaDoc s) (SchemaIn r) (SchemaOut w) v <- schemaOut sch x pure [Key.fromText name A..= v] - s = mkDocF @doc @f (mkField name (schemaDoc sch)) + s = mkDoc (mkField name (schemaDoc sch)) -- | Like 'fieldOverF', but specialised to the identity functor. fieldOver :: @@ -364,7 +357,13 @@ fieldOver :: Text -> SchemaP doc' v' A.Value a b -> SchemaP doc v [A.Pair] a b -fieldOver l name = fmap runIdentity . fieldOverF l name +fieldOver l name = + fmap runIdentity + . fieldOverF + (\f obj key -> Identity <$> A.explicitParseField f obj (Key.fromText key)) + id + l + name -- | Like 'field', but apply an arbitrary function to the -- documentation of the field. @@ -388,17 +387,6 @@ optFieldWithDocModifier :: SchemaP doc A.Object [A.Pair] a (Maybe b) optFieldWithDocModifier name modify sch = optField @doc @doc' name (over doc modify sch) --- | Like 'fieldF', but apply an arbitrary function to the --- documentation of the field. -fieldWithDocModifierF :: - forall doc' doc f a b. - (HasField doc' doc, FieldFunctor doc f) => - Text -> - (doc' -> doc') -> - SchemaP doc' A.Value A.Value a b -> - SchemaP doc A.Object [A.Pair] a (f b) -fieldWithDocModifierF name modify sch = fieldF @doc' @doc name (over doc modify sch) - -- | Change the input type of a schema. (.=) :: (Profunctor p) => (a -> a') -> p a' b -> p a b (.=) = lmap diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index d98551fad62..f838f5fa0b6 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -850,6 +850,7 @@ data ClassifiedDomainsConfig = ClassifiedDomainsConfig instance ParseDbFeature ClassifiedDomainsConfig where parseDbConfig _ = fail "ClassifiedDomainsConfig cannot be parsed from the DB" + serialiseDbConfig = DbConfig . schemaToJSON instance Default ClassifiedDomainsConfig where def = ClassifiedDomainsConfig [] @@ -903,12 +904,12 @@ deriving via (BarbieFeature AppLockConfigB) instance ToSchema AppLockConfig instance Default AppLockConfig where def = AppLockConfig (EnforceAppLock False) 60 -instance (FieldFunctor SwaggerDoc f) => ToSchema (AppLockConfigB Covered f) where +instance (FieldF f) => ToSchema (AppLockConfigB Covered f) where schema = object "AppLockConfig" $ AppLockConfig - <$> (.enforce) .= extractF (fieldF "enforceAppLock" schema) - <*> (.timeout) .= extractF (fieldF "inactivityTimeoutSecs" schema) + <$> (.enforce) .= fieldF "enforceAppLock" schema + <*> (.timeout) .= fieldF "inactivityTimeoutSecs" schema instance Default (LockableFeature AppLockConfig) where def = defUnlockedFeature @@ -981,11 +982,11 @@ deriving via (BarbieFeature SelfDeletingMessagesConfigB) instance (ToSchema Self instance Default SelfDeletingMessagesConfig where def = SelfDeletingMessagesConfig 0 -instance (FieldFunctor SwaggerDoc f) => ToSchema (SelfDeletingMessagesConfigB Covered f) where +instance (FieldF f) => ToSchema (SelfDeletingMessagesConfigB Covered f) where schema = object "SelfDeletingMessagesConfig" $ SelfDeletingMessagesConfig - <$> sdmEnforcedTimeoutSeconds .= extractF (fieldF "enforcedTimeoutSeconds" schema) + <$> sdmEnforcedTimeoutSeconds .= fieldF "enforcedTimeoutSeconds" schema instance Default (LockableFeature SelfDeletingMessagesConfig) where def = defUnlockedFeature @@ -1036,15 +1037,20 @@ instance Default MLSConfig where MLS_128_DHKEMP256_AES128GCM_SHA256_P256 [ProtocolProteusTag, ProtocolMLSTag] -instance (FieldFunctor SwaggerDoc f) => ToSchema (MLSConfigB Covered f) where +instance (FieldF f) => ToSchema (MLSConfigB Covered f) where schema = object "MLSConfig" $ MLSConfig - <$> mlsProtocolToggleUsers .= extractF (fieldWithDocModifierF "protocolToggleUsers" (S.description ?~ "allowlist of users that may change protocols") (array schema)) - <*> mlsDefaultProtocol .= extractF (fieldF "defaultProtocol" schema) - <*> mlsAllowedCipherSuites .= extractF (fieldF "allowedCipherSuites" (array schema)) - <*> mlsDefaultCipherSuite .= extractF (fieldF "defaultCipherSuite" schema) - <*> mlsSupportedProtocols .= extractF (fieldF "supportedProtocols" (array schema)) + <$> mlsProtocolToggleUsers + .= ( fieldWithDocModifierF + "protocolToggleUsers" + (S.description ?~ "allowlist of users that may change protocols") + (array schema) + ) + <*> mlsDefaultProtocol .= fieldF "defaultProtocol" schema + <*> mlsAllowedCipherSuites .= fieldF "allowedCipherSuites" (array schema) + <*> mlsDefaultCipherSuite .= fieldF "defaultCipherSuite" schema + <*> mlsSupportedProtocols .= fieldF "supportedProtocols" (array schema) instance Default (LockableFeature MLSConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -1150,12 +1156,16 @@ instance Arbitrary MlsE2EIdConfig where <*> fmap (Alt . pure) arbitrary <*> arbitrary -instance (FieldFunctor SwaggerDoc f) => ToSchema (MlsE2EIdConfigB Covered f) where +instance (FieldF f) => ToSchema (MlsE2EIdConfigB Covered f) where schema = object "MlsE2EIdConfig" $ MlsE2EIdConfig - <$> (fmap toSeconds . verificationExpiration) - .= extractF (fieldWithDocModifierF "verificationExpiration" veDesc (fromSeconds <$> schema)) + <$> ( (fmap toSeconds . verificationExpiration) + .= fieldWithDocModifierF + "verificationExpiration" + (description ?~ veDesc) + (fromSeconds <$> schema) + ) <*> (getAlt . acmeDiscoveryUrl) .= fmap Alt (maybe_ (optField "acmeDiscoveryUrl" schema)) <*> (getAlt . crlProxy) .= fmap Alt (maybe_ (optField "crlProxy" schema)) @@ -1167,20 +1177,19 @@ instance (FieldFunctor SwaggerDoc f) => ToSchema (MlsE2EIdConfigB Covered f) whe toSeconds :: NominalDiffTime -> Int toSeconds = truncate - veDesc :: NamedSwaggerDoc -> NamedSwaggerDoc + veDesc :: Text veDesc = - 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`." + "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`." instance Default (LockableFeature MlsE2EIdConfig) where def = defLockedFeature @@ -1263,8 +1272,12 @@ type EnforceFileDownloadLocationConfig = EnforceFileDownloadLocationConfigB Bare deriving instance (Eq EnforceFileDownloadLocationConfig) +deriving instance (Eq (EnforceFileDownloadLocationConfigB Covered Maybe)) + deriving instance (Show EnforceFileDownloadLocationConfig) +deriving instance (Show (EnforceFileDownloadLocationConfigB Covered Maybe)) + deriving via (RenderableTypeName EnforceFileDownloadLocationConfig) instance (RenderableSymbol EnforceFileDownloadLocationConfig) deriving via (BarbieFeature EnforceFileDownloadLocationConfigB) instance (ToSchema EnforceFileDownloadLocationConfig) @@ -1524,11 +1537,13 @@ deriving via (Schema AllTeamFeatures) instance (S.ToSchema AllTeamFeatures) class ParseDbFeature cfg where parseDbConfig :: DbConfig -> A.Parser (cfg -> cfg) + serialiseDbConfig :: cfg -> DbConfig newtype TrivialFeature cfg = TrivialFeature cfg instance (GSOP.IsProductType cfg '[]) => ParseDbFeature (TrivialFeature cfg) where parseDbConfig _ = pure id + serialiseDbConfig _ = def instance (GSOP.IsProductType cfg '[]) => Default (TrivialFeature cfg) where def = TrivialFeature (GSOP.productTypeTo Nil) @@ -1551,6 +1566,12 @@ instance applyConfig :: b Bare Identity -> b Covered Maybe -> b Bare Identity applyConfig cfg1 cfg2 = bstrip $ bzipWith f cfg2 (bcover cfg1) + serialiseDbConfig = + DbConfig + . schemaToJSON + . bmap (Just . runIdentity) + . bcover + . unBarbieFeature instance (BareB b, ToSchema (b Covered Identity)) => ToSchema (BarbieFeature b) where schema = (bcover . unBarbieFeature) .= fmap (BarbieFeature . bstrip) schema @@ -1573,7 +1594,7 @@ serialiseDbFeature feat = LockableFeaturePatch { status = Just feat.status, lockStatus = Just feat.lockStatus, - config = Just . DbConfig . schemaToJSON $ feat.config + config = Just $ serialiseDbConfig feat.config } -- | Convert a map indexed by feature name to an NP value. diff --git a/libs/wire-api/src/Wire/API/Team/Feature/Profunctor.hs b/libs/wire-api/src/Wire/API/Team/Feature/Profunctor.hs index c6331cd6e3e..e497b87933c 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature/Profunctor.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature/Profunctor.hs @@ -23,11 +23,34 @@ -- (for database values). module Wire.API.Team.Feature.Profunctor where +import Control.Lens ((%~)) import Data.Default import Data.Profunctor import Data.Schema import Imports +class (Functor f) => FieldF f where + fieldF :: + (HasOpt doc', HasField doc doc') => + Text -> + ValueSchemaP doc a b -> + ObjectSchemaP doc' (f a) (f b) + +fieldWithDocModifierF :: + forall f doc' doc a b. + (FieldF f, HasOpt doc', HasField doc doc') => + Text -> + (doc' -> doc') -> + ValueSchemaP doc a b -> + ObjectSchemaP doc' (f a) (f b) +fieldWithDocModifierF name f sch = fieldF @f @doc' name sch & doc %~ f + +instance FieldF Maybe where + fieldF name sch = maybe_ (optField name sch) + +instance FieldF Identity where + fieldF name sch = Identity <$> runIdentity .= field name sch + -- | Parse an optional field by using its default when @f@ is 'Identity', and -- leaving it as 'Nothing' when @f@ is 'Maybe'. class OptWithDefault f where @@ -47,7 +70,8 @@ class NestedMaybe f where nestedMaybeField :: Text -> ValueSchema SwaggerDoc a -> ObjectSchema SwaggerDoc (f (Maybe a)) instance NestedMaybe Maybe where - nestedMaybeField name sch = maybe_ (optField name (nullable sch)) + nestedMaybeField name sch = + maybe_ (optField' name (nullable sch)) instance NestedMaybe Identity where nestedMaybeField name sch = Identity <$> runIdentity .= maybe_ (optField name sch) diff --git a/libs/wire-api/src/Wire/API/User/RichInfo.hs b/libs/wire-api/src/Wire/API/User/RichInfo.hs index 3796d811077..62b766843d7 100644 --- a/libs/wire-api/src/Wire/API/User/RichInfo.hs +++ b/libs/wire-api/src/Wire/API/User/RichInfo.hs @@ -157,7 +157,7 @@ ciField :: ciField name sch = mkSchema s r w where s :: doc' - s = mkDocF @doc' @Identity (mkField (CI.original name) (schemaDoc sch)) & desc + s = mkField (CI.original name) (schemaDoc sch) & desc where desc = S.description ?~ ("json field with case-insensitive keys." :: Text) @@ -177,7 +177,7 @@ ciOptField :: ciOptField name sch = mkSchema s r w where s :: doc - s = mkDocF @doc @Identity (mkField (CI.original name) (schemaDoc sch)) & desc + s = mkField (CI.original name) (schemaDoc sch) & desc where desc = S.description ?~ ("optional json field with case-insensitive keys." :: Text) From 70d2c310eefc1ea5a58e1a695eb536375f5c575b Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 25 Feb 2025 08:29:47 +0100 Subject: [PATCH 22/25] Fix MLS feature name --- 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 f838f5fa0b6..584552b8f04 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1020,7 +1020,7 @@ deriving instance Eq MLSConfig deriving instance Show MLSConfig -deriving via (RenderableTypeName GuestLinksConfig) instance (RenderableSymbol MLSConfig) +deriving via (RenderableTypeName MLSConfig) instance (RenderableSymbol MLSConfig) deriving via (GenericUniform MLSConfig) instance (Arbitrary MLSConfig) From 7ab99e3e6ff51a5ecfd4f2e89fafe8f0df5ff58b Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 21 Feb 2025 11:21:16 +0100 Subject: [PATCH 23/25] Remove old feature storage --- .../src/Wire/API/Routes/Internal/Galley.hs | 10 - services/galley/galley.cabal | 7 - services/galley/src/Galley/API/Internal.hs | 3 - .../galley/src/Galley/Cassandra/FeatureTH.hs | 53 -- .../Galley/Cassandra/GetAllTeamFeatures.hs | 86 --- .../src/Galley/Cassandra/MakeFeature.hs | 551 ------------------ .../galley/src/Galley/Cassandra/Orphans.hs | 8 - .../src/Galley/Cassandra/TeamFeatures.hs | 106 +--- .../src/Galley/Effects/TeamFeatureStore.hs | 4 - 9 files changed, 4 insertions(+), 824 deletions(-) delete mode 100644 services/galley/src/Galley/Cassandra/FeatureTH.hs delete mode 100644 services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs delete mode 100644 services/galley/src/Galley/Cassandra/MakeFeature.hs delete mode 100644 services/galley/src/Galley/Cassandra/Orphans.hs 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 fbff39b629b..3daa6448544 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -99,16 +99,6 @@ type IFeatureAPI = :> Get '[JSON] AllTeamFeatures ) :<|> IFeatureStatusLockStatusPut DomainRegistrationConfig - -- migration state - :<|> Named - "put-feature-migration-state" - ( Summary "Manually set migration state (for testing)" - :> "teams" - :> Capture "team" TeamId - :> "feature-migration-state" - :> ReqBody '[JSON] TeamFeatureMigrationState - :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "OK") - ) type InternalAPI = "i" :> InternalAPIBase diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index c141e005f06..624eea9f78f 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -139,12 +139,8 @@ library Galley.Cassandra.Conversation.MLS Galley.Cassandra.ConversationList Galley.Cassandra.CustomBackend - Galley.Cassandra.FeatureTH - Galley.Cassandra.GetAllTeamFeatures Galley.Cassandra.Instances Galley.Cassandra.LegalHold - Galley.Cassandra.MakeFeature - Galley.Cassandra.Orphans Galley.Cassandra.Proposal Galley.Cassandra.Queries Galley.Cassandra.SearchVisibility @@ -319,7 +315,6 @@ library , extended , extra >=1.3 , galley-types >=0.65.0 - , generics-sop , hex , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk @@ -348,13 +343,11 @@ library , servant-client , servant-server , singletons - , singletons-base , sop-core , split >=0.2 , ssl-util >=0.1 , stm >=2.4 , tagged - , template-haskell , text >=0.11 , time >=1.4 , tinylog >=0.10 diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index e634b1de2c5..27433530eec 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -61,7 +61,6 @@ import Galley.Effects.CustomBackendStore import Galley.Effects.LegalHoldStore as LegalHoldStore import Galley.Effects.MemberStore qualified as E import Galley.Effects.ServiceStore -import Galley.Effects.TeamFeatureStore qualified as E import Galley.Effects.TeamStore import Galley.Effects.TeamStore qualified as E import Galley.Monad @@ -289,8 +288,6 @@ featureAPI = -- all features <@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser) <@> mkNamedAPI @'("ilock", DomainRegistrationConfig) (updateLockStatus @DomainRegistrationConfig) - -- migration state - <@> mkNamedAPI @"put-feature-migration-state" E.setMigrationState rmUser :: forall p1 p2 r. diff --git a/services/galley/src/Galley/Cassandra/FeatureTH.hs b/services/galley/src/Galley/Cassandra/FeatureTH.hs deleted file mode 100644 index cf52cdc6caf..00000000000 --- a/services/galley/src/Galley/Cassandra/FeatureTH.hs +++ /dev/null @@ -1,53 +0,0 @@ -{-# LANGUAGE QuasiQuotes #-} -{-# LANGUAGE TemplateHaskellQuotes #-} - -module Galley.Cassandra.FeatureTH where - -import Data.Kind -import Generics.SOP.TH -import Imports -import Language.Haskell.TH hiding (Type) -import Wire.API.Team.Feature - -featureCases :: ExpQ -> Q Exp -featureCases rhsQ = do - rhs <- rhsQ - TyConI (DataD _ _ _ _ constructors _) <- reify ''FeatureSingleton - pure $ - LamCaseE - [ Match (ConP c [] []) (NormalB rhs) [] - | GadtC [c] _ _ <- constructors - ] - -generateTupleP :: Q [Dec] -generateTupleP = do - let maxSize = 64 :: Int - tylist <- [t|[Type]|] - let vars = [VarT (mkName ("a" <> show i)) | i <- [0 .. maxSize - 1]] - pure - [ ClosedTypeFamilyD - (TypeFamilyHead (mkName "TupleP") [KindedTV (mkName "xs") () tylist] NoSig Nothing) - [ TySynEqn - Nothing - ( ConT (mkName "TupleP") - `AppT` mkPattern (take n vars) - ) - (mkTuple (take n vars)) - | n <- [0 .. maxSize] - ] - ] - where - mkPattern = foldr (\x y -> PromotedConsT `AppT` x `AppT` y) PromotedNilT - - mkTuple [] = ConT ''() - mkTuple [v] = ConT ''Identity `AppT` v - mkTuple vs = - let n = length vs - in foldl' AppT (TupleT n) vs - --- | generates some of the remaining @SOP.Generic@ instances as orphans --- it is cut off at 50 on purpose to reduce compilation times --- you may increase up to 64 which is the number at which you --- you should probably start fixing cql instead. -generateSOPInstances :: Q [Dec] -generateSOPInstances = concat <$> traverse (deriveGeneric . tupleTypeName) [31 .. 50] diff --git a/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs b/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs deleted file mode 100644 index 3b378b65816..00000000000 --- a/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs +++ /dev/null @@ -1,86 +0,0 @@ -{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-} - -module Galley.Cassandra.GetAllTeamFeatures (getAllDbFeaturesLegacy) where - -import Cassandra -import Data.Id -import Galley.Cassandra.Instances () -import Galley.Cassandra.MakeFeature -import Galley.Cassandra.Orphans () -import Generics.SOP -import Imports hiding (Map) -import Polysemy.Internal -import Wire.API.Team.Feature - -type family ConcatFeatureRow xs where - ConcatFeatureRow '[] = '[] - ConcatFeatureRow (x : xs) = Append (FeatureRow x) (ConcatFeatureRow xs) - -type AllFeatureRow = ConcatFeatureRow Features - -emptyRow :: NP Maybe AllFeatureRow -emptyRow = hpure Nothing - -class ConcatFeatures cfgs where - rowToAllFeatures :: NP Maybe (ConcatFeatureRow cfgs) -> NP DbFeature cfgs - -instance ConcatFeatures '[] where - rowToAllFeatures Nil = Nil - -instance - ( SplitNP (FeatureRow cfg) (ConcatFeatureRow cfgs), - ConcatFeatures cfgs, - MakeFeature cfg - ) => - ConcatFeatures (cfg : cfgs) - where - rowToAllFeatures row = case splitNP @(FeatureRow cfg) @(ConcatFeatureRow cfgs) row of - (row0, row1) -> rowToFeature row0 :* rowToAllFeatures row1 - -class SplitNP xs ys where - splitNP :: NP f (Append xs ys) -> (NP f xs, NP f ys) - -instance SplitNP '[] ys where - splitNP ys = (Nil, ys) - -instance (SplitNP xs ys) => SplitNP (x ': xs) ys where - splitNP (z :* zs) = case splitNP zs of - (xs, ys) -> (z :* xs, ys) - -class AppendNP xs ys where - appendNP :: NP f xs -> NP f ys -> NP f (Append xs ys) - -instance AppendNP '[] ys where - appendNP Nil ys = ys - -instance (AppendNP xs ys) => AppendNP (x : xs) ys where - appendNP (x :* xs) ys = x :* appendNP xs ys - -class ConcatColumns cfgs where - concatColumns :: NP (K String) (ConcatFeatureRow cfgs) - -instance ConcatColumns '[] where - concatColumns = Nil - -instance - ( AppendNP (FeatureRow cfg) (ConcatFeatureRow cfgs), - MakeFeature cfg, - ConcatColumns cfgs - ) => - ConcatColumns (cfg : cfgs) - where - concatColumns = featureColumns @cfg `appendNP` concatColumns @cfgs - -getAllDbFeaturesLegacy :: - forall row mrow m. - ( row ~ AllFeatureRow, - Tuple (TupleP mrow), - IsProductType (TupleP mrow) mrow, - AllZip (IsF Maybe) row mrow, - MonadClient m - ) => - TeamId -> - m (AllFeatures DbFeature) -getAllDbFeaturesLegacy tid = do - mRow <- fetchFeatureRow @row @mrow tid (concatColumns @Features) - pure . rowToAllFeatures $ fromMaybe emptyRow mRow diff --git a/services/galley/src/Galley/Cassandra/MakeFeature.hs b/services/galley/src/Galley/Cassandra/MakeFeature.hs deleted file mode 100644 index 7f87388764e..00000000000 --- a/services/galley/src/Galley/Cassandra/MakeFeature.hs +++ /dev/null @@ -1,551 +0,0 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE TemplateHaskell #-} - --- | Abstraction to fetch and store feature values from and to the database. -module Galley.Cassandra.MakeFeature where - -import Cassandra -import Cassandra qualified as C -import Data.Functor -import Data.Functor.Identity -import Data.Id -import Data.Kind -import Data.List.Singletons (Length) -import Data.Misc (HttpsUrl) -import Data.Singletons (demote) -import Data.Time -import Data.Time.Clock.POSIX -import GHC.TypeNats -import Galley.Cassandra.FeatureTH -import Galley.Cassandra.Instances () -import Generics.SOP -import Imports hiding (Generic, Map) -import Wire.API.Conversation.Protocol (ProtocolTag) -import Wire.API.MLS.CipherSuite -import Wire.API.Team.Feature - --- [Note: default values for configuration fields] --- --- When reading values for configuration types with multiple fields, we fall --- back to default values for each field independently, instead of treating the --- whole configuration as a single value that can be set or not. --- --- In most cases, either strategy would produce the same result, because there --- is no way to set only *some* fields using the public API. However, that can --- happen when a feature flag changes over time and gains new fields, as it has --- been the case for mlsE2EId. --- --- Therefore, we use the first strategy consistently for all feature flags, --- even when it does not matter. - --- | This is necessary in order to convert an @NP f xs@ type to something that --- CQL can understand. --- --- The generated code looks like: --- @@ --- instance TupleP xs where --- TupleP '[] = () --- TupleP '[a] = Identity a --- TupleP '[a, b] = (a, b) --- ... --- @@ -$generateTupleP - -class MakeFeature cfg where - type FeatureRow cfg :: [Type] - type FeatureRow cfg = '[FeatureStatus] - - featureColumns :: NP (K String) (FeatureRow cfg) - - rowToFeature :: NP Maybe (FeatureRow cfg) -> DbFeature cfg - default rowToFeature :: - (FeatureRow cfg ~ '[FeatureStatus]) => - NP Maybe (FeatureRow cfg) -> - DbFeature cfg - rowToFeature = foldMap dbFeatureStatus . hd - - featureToRow :: LockableFeature cfg -> NP Maybe (FeatureRow cfg) - default featureToRow :: - (FeatureRow cfg ~ '[FeatureStatus]) => - LockableFeature cfg -> - NP Maybe (FeatureRow cfg) - featureToRow feat = Just feat.status :* Nil - -instance MakeFeature LegalholdConfig where - featureColumns = K "legalhold_status" :* Nil - -instance MakeFeature SSOConfig where - featureColumns = K "sso_status" :* Nil - -instance MakeFeature SearchVisibilityAvailableConfig where - featureColumns = K "search_visibility_status" :* Nil - --- | This feature shares its status column with --- 'SearchVisibilityAvailableConfig'. This means that when fetching all --- features, this column is repeated in the query, i.e. the query looks like: --- @@ --- select ..., search_visibility_status, search_visibility_status, ... from team_features ... --- @@ -instance MakeFeature SearchVisibilityInboundConfig where - featureColumns = K "search_visibility_status" :* Nil - -instance MakeFeature ValidateSAMLEmailsConfig where - featureColumns = K "validate_saml_emails" :* Nil - -instance MakeFeature DigitalSignaturesConfig where - featureColumns = K "digital_signatures" :* Nil - -instance MakeFeature AppLockConfig where - type FeatureRow AppLockConfig = '[FeatureStatus, EnforceAppLock, Int32] - featureColumns = - K "app_lock_status" - :* K "app_lock_enforce" - :* K "app_lock_inactivity_timeout_secs" - :* Nil - - rowToFeature (status :* enforce :* timeout :* Nil) = - foldMap dbFeatureStatus status - -- [Note: default values for configuration fields] - <> dbFeatureModConfig - ( \defCfg -> - AppLockConfig - (fromMaybe defCfg.enforce enforce) - (fromMaybe defCfg.timeout timeout) - ) - - featureToRow feat = - Just feat.status - :* Just feat.config.enforce - :* Just feat.config.timeout - :* Nil - -instance MakeFeature ClassifiedDomainsConfig where - type FeatureRow ClassifiedDomainsConfig = '[] - featureColumns = Nil - - rowToFeature Nil = mempty - featureToRow _ = Nil - -instance MakeFeature FileSharingConfig where - type FeatureRow FileSharingConfig = '[LockStatus, FeatureStatus] - featureColumns = K "file_sharing_lock_status" :* K "file_sharing" :* Nil - - rowToFeature (lockStatus :* status :* Nil) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - - featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil - -instance MakeFeature ConferenceCallingConfig where - type FeatureRow ConferenceCallingConfig = '[LockStatus, FeatureStatus, One2OneCalls] - featureColumns = - K "conference_calling" - :* K "conference_calling_status" - :* K "conference_calling_one_to_one" - :* Nil - - rowToFeature (lockStatus :* status :* calls :* Nil) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - <> foldMap (dbFeatureConfig . ConferenceCallingConfig) calls - - featureToRow feat = - Just feat.lockStatus - :* Just feat.status - :* Just feat.config.one2OneCalls - :* Nil - -instance MakeFeature SelfDeletingMessagesConfig where - type FeatureRow SelfDeletingMessagesConfig = '[LockStatus, FeatureStatus, Int32] - featureColumns = - K "self_deleting_messages_lock_status" - :* K "self_deleting_messages_status" - :* K "self_deleting_messages_ttl" - :* Nil - - rowToFeature (lockStatus :* status :* ttl :* Nil) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - <> foldMap (dbFeatureConfig . SelfDeletingMessagesConfig) ttl - - featureToRow feat = - Just feat.lockStatus - :* Just feat.status - :* Just feat.config.sdmEnforcedTimeoutSeconds - :* Nil - -instance MakeFeature GuestLinksConfig where - type FeatureRow GuestLinksConfig = '[LockStatus, FeatureStatus] - featureColumns = K "guest_links_lock_status" :* K "guest_links_status" :* Nil - - rowToFeature (lockStatus :* status :* Nil) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - - featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil - -instance MakeFeature SndFactorPasswordChallengeConfig where - type FeatureRow SndFactorPasswordChallengeConfig = '[LockStatus, FeatureStatus] - featureColumns = - K "snd_factor_password_challenge_lock_status" - :* K "snd_factor_password_challenge_status" - :* Nil - - rowToFeature (lockStatus :* status :* Nil) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - - featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil - -instance MakeFeature ExposeInvitationURLsToTeamAdminConfig where - featureColumns = K "expose_invitation_urls_to_team_admin" :* Nil - -instance MakeFeature OutlookCalIntegrationConfig where - type FeatureRow OutlookCalIntegrationConfig = '[LockStatus, FeatureStatus] - - featureColumns = - K "outlook_cal_integration_lock_status" - :* K "outlook_cal_integration_status" - :* Nil - - rowToFeature (lockStatus :* status :* Nil) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - - featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil - -instance MakeFeature DomainRegistrationConfig where - type FeatureRow DomainRegistrationConfig = '[LockStatus, FeatureStatus] - - featureColumns = - K "domain_registration_lock_status" - :* K "domain_registration_status" - :* Nil - - rowToFeature (lockStatus :* status :* Nil) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - - featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil - -instance MakeFeature MLSConfig where - type - FeatureRow MLSConfig = - '[ LockStatus, - FeatureStatus, - ProtocolTag, - (C.Set UserId), - (C.Set CipherSuiteTag), - CipherSuiteTag, - (C.Set ProtocolTag) - ] - featureColumns = - K "mls_lock_status" - :* K "mls_status" - :* K "mls_default_protocol" - :* K "mls_protocol_toggle_users" - :* K "mls_allowed_ciphersuites" - :* K "mls_default_ciphersuite" - :* K "mls_supported_protocols" - :* Nil - - rowToFeature - ( lockStatus - :* status - :* defProto - :* toggleUsers - :* ciphersuites - :* defCiphersuite - :* supportedProtos - :* Nil - ) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - <> dbFeatureModConfig - ( \defCfg -> - -- [Note: default values for configuration fields] - -- - -- This case is a bit special, because Cassandra sets do not - -- distinguish between 'null' and 'empty'. To differentiate - -- between these cases, we use the `mls_default_protocol` field: - -- if set, we interpret null sets as empty, otherwise we use the - -- default. - let configIsSet = isJust defProto - in MLSConfig - ( maybe - (if configIsSet then [] else defCfg.mlsProtocolToggleUsers) - C.fromSet - toggleUsers - ) - (fromMaybe defCfg.mlsDefaultProtocol defProto) - ( maybe - (if configIsSet then [] else defCfg.mlsAllowedCipherSuites) - C.fromSet - ciphersuites - ) - (fromMaybe defCfg.mlsDefaultCipherSuite defCiphersuite) - ( maybe - (if configIsSet then [] else defCfg.mlsSupportedProtocols) - C.fromSet - supportedProtos - ) - ) - - featureToRow feat = - Just feat.lockStatus - :* Just feat.status - :* Just feat.config.mlsDefaultProtocol - :* Just (C.Set feat.config.mlsProtocolToggleUsers) - :* Just (C.Set feat.config.mlsAllowedCipherSuites) - :* Just feat.config.mlsDefaultCipherSuite - :* Just (C.Set feat.config.mlsSupportedProtocols) - :* Nil - -instance MakeFeature MlsE2EIdConfig where - type - FeatureRow MlsE2EIdConfig = - '[ LockStatus, - FeatureStatus, - Int32, - HttpsUrl, - HttpsUrl, - Bool - ] - featureColumns = - K "mls_e2eid_lock_status" - :* K "mls_e2eid_status" - :* K "mls_e2eid_grace_period" - :* K "mls_e2eid_acme_discovery_url" - :* K "mls_e2eid_crl_proxy" - :* K "mls_e2eid_use_proxy_on_mobile" - :* Nil - - rowToFeature - ( lockStatus - :* status - :* gracePeriod - :* acmeDiscoveryUrl - :* crlProxy - :* useProxyOnMobile - :* Nil - ) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - <> dbFeatureModConfig - ( \defCfg -> - defCfg - { verificationExpiration = - maybe defCfg.verificationExpiration fromIntegral gracePeriod, - acmeDiscoveryUrl = Alt acmeDiscoveryUrl <|> defCfg.acmeDiscoveryUrl, - crlProxy = Alt crlProxy <|> defCfg.crlProxy, - useProxyOnMobile = - maybe defCfg.useProxyOnMobile UseProxyOnMobile useProxyOnMobile - } - ) - - featureToRow feat = - Just feat.lockStatus - :* Just feat.status - :* Just (truncate feat.config.verificationExpiration) - :* getAlt feat.config.acmeDiscoveryUrl - :* getAlt feat.config.crlProxy - :* Just (unUseProxyOnMobile feat.config.useProxyOnMobile) - :* Nil - --- Optional time stamp. A 'Nothing' value is represented as 0. -newtype OptionalUTCTime = OptionalUTCTime {unOptionalUTCTime :: Maybe UTCTime} - -instance Cql OptionalUTCTime where - ctype = Tagged (untag (ctype @UTCTime)) - - toCql = toCql . fromMaybe (posixSecondsToUTCTime 0) . unOptionalUTCTime - - fromCql x = do - t <- fromCql x - pure . OptionalUTCTime $ guard (utcTimeToPOSIXSeconds t /= 0) $> t - -instance MakeFeature MlsMigrationConfig where - type - FeatureRow MlsMigrationConfig = - '[LockStatus, FeatureStatus, OptionalUTCTime, OptionalUTCTime] - - featureColumns = - K "mls_migration_lock_status" - :* K "mls_migration_status" - :* K "mls_migration_start_time" - :* K "mls_migration_finalise_regardless_after" - :* Nil - - rowToFeature (lockStatus :* status :* startTime :* finalizeAfter :* Nil) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - <> dbFeatureModConfig - ( \defCfg -> - defCfg - { startTime = maybe defCfg.startTime unOptionalUTCTime startTime, - finaliseRegardlessAfter = - maybe - defCfg.finaliseRegardlessAfter - unOptionalUTCTime - finalizeAfter - } - ) - - featureToRow feat = - Just feat.lockStatus - :* Just feat.status - :* Just (OptionalUTCTime feat.config.startTime) - :* Just (OptionalUTCTime feat.config.finaliseRegardlessAfter) - :* Nil - -instance MakeFeature EnforceFileDownloadLocationConfig where - type FeatureRow EnforceFileDownloadLocationConfig = '[LockStatus, FeatureStatus, Text] - - featureColumns = - K "enforce_file_download_location_lock_status" - :* K "enforce_file_download_location_status" - :* K "enforce_file_download_location" - :* Nil - - rowToFeature (lockStatus :* status :* location :* Nil) = - foldMap dbFeatureLockStatus lockStatus - <> foldMap dbFeatureStatus status - <> foldMap - dbFeatureConfig - ( case location of - Nothing -> Nothing - -- convert empty string to 'Nothing' - Just "" -> Just (EnforceFileDownloadLocationConfig Nothing) - Just loc -> Just (EnforceFileDownloadLocationConfig (Just loc)) - ) - - featureToRow feat = - Just feat.lockStatus - :* Just feat.status - -- represent 'Nothing' as the empty string - :* Just (fromMaybe "" feat.config.enforcedDownloadLocation) - :* Nil - -instance MakeFeature LimitedEventFanoutConfig where - featureColumns = K "limited_event_fanout_status" :* Nil - -fetchFeature :: - forall cfg m row mrow. - ( MonadClient m, - row ~ FeatureRow cfg, - MakeFeature cfg, - IsProductType (TupleP mrow) mrow, - AllZip (IsF Maybe) row mrow, - Tuple (TupleP mrow) - ) => - TeamId -> - m (DbFeature cfg) -fetchFeature tid = do - case featureColumns @cfg of - Nil -> pure (rowToFeature Nil) - cols -> do - mRow <- fetchFeatureRow @row @mrow tid cols - pure $ foldMap rowToFeature mRow - -fetchFeatureRow :: - forall row mrow m. - ( MonadClient m, - IsProductType (TupleP mrow) mrow, - AllZip (IsF Maybe) row mrow, - Tuple (TupleP mrow) - ) => - TeamId -> - NP (K String) row -> - m (Maybe (NP Maybe row)) -fetchFeatureRow tid cols = do - let select :: PrepQuery R (Identity TeamId) (TupleP mrow) - select = - fromString $ - "select " - <> intercalate ", " (hcollapse cols) - <> " from team_features where team_id = ?" - row <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) - pure $ fmap (unfactorI . productTypeFrom) row - -storeFeature :: - forall cfg m row mrow. - ( MonadClient m, - row ~ FeatureRow cfg, - MakeFeature cfg, - IsProductType (TupleP (TeamId : mrow)) (TeamId : mrow), - AllZip (IsF Maybe) row mrow, - Tuple (TupleP (TeamId : mrow)), - KnownNat (Length row) - ) => - TeamId -> - LockableFeature cfg -> - m () -storeFeature tid feat = do - if n == 0 - then pure () - else - retry x5 $ - write - insert - ( params LocalQuorum (productTypeTo (I tid :* factorI (featureToRow feat))) - ) - where - n :: Int - n = fromIntegral (demote @(Length row)) - - insert :: PrepQuery W (TupleP (TeamId ': mrow)) () - insert = - fromString $ - "insert into team_features (team_id, " - <> intercalate ", " (hcollapse (featureColumns @cfg)) - <> ") values (" - <> intercalate "," (replicate (succ n) "?") - <> ")" - -class (FeatureRow cfg ~ row) => StoreFeatureLockStatus (row :: [Type]) cfg where - storeFeatureLockStatus' :: (MonadClient m) => TeamId -> Tagged cfg LockStatus -> m () - -instance - {-# OVERLAPPING #-} - ( FeatureRow cfg ~ (LockStatus ': row), - MakeFeature cfg - ) => - StoreFeatureLockStatus (LockStatus ': row) cfg - where - storeFeatureLockStatus' tid lock = do - let col = unK (hd (featureColumns @cfg)) - insert :: PrepQuery W (TeamId, LockStatus) () - insert = - fromString $ - "insert into team_features (team_id, " <> col <> ") values (?, ?)" - retry x5 $ write insert (params LocalQuorum (tid, (untag lock))) - -instance (FeatureRow cfg ~ row) => StoreFeatureLockStatus row cfg where - storeFeatureLockStatus' _ _ = pure () - -storeFeatureLockStatus :: - forall cfg m. - (MonadClient m, StoreFeatureLockStatus (FeatureRow cfg) cfg) => - TeamId -> - Tagged cfg LockStatus -> - m () -storeFeatureLockStatus = storeFeatureLockStatus' @(FeatureRow cfg) - --- | Convert @NP f [x1, ..., xn]@ to @NP I [f x1, ..., f xn]@. --- --- This works because @I . f = f@. -factorI :: forall f xs ys. (AllZip (IsF f) xs ys) => NP f xs -> NP I ys -factorI Nil = Nil -factorI (x :* xs) = I x :* factorI xs - --- | Convert @NP I [f x1, ..., f xn]@ to @NP f [x1, ..., xn]@. --- --- See 'factorI'. -unfactorI :: forall f xs ys. (AllZip (IsF f) xs ys) => NP I ys -> NP f xs -unfactorI Nil = Nil -unfactorI (I x :* xs) = x :* unfactorI xs - --- | This is to emulate a constraint-level lambda. -class (f x ~ y) => IsF f x y | y -> x - -instance (f x ~ y) => IsF f x y diff --git a/services/galley/src/Galley/Cassandra/Orphans.hs b/services/galley/src/Galley/Cassandra/Orphans.hs deleted file mode 100644 index d939cdafdb0..00000000000 --- a/services/galley/src/Galley/Cassandra/Orphans.hs +++ /dev/null @@ -1,8 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} -{-# OPTIONS_GHC -Wno-orphans #-} - -module Galley.Cassandra.Orphans where - -import Galley.Cassandra.FeatureTH - -$generateSOPInstances diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 4a4608bcab8..faf320ca525 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -27,16 +27,12 @@ where import Cassandra import Data.Aeson.Types qualified as A import Data.Constraint -import Data.Default import Data.Id import Data.Map qualified as M import Data.Text.Lazy qualified as LT import Galley.API.Error import Galley.API.Teams.Features.Get -import Galley.Cassandra.FeatureTH -import Galley.Cassandra.GetAllTeamFeatures import Galley.Cassandra.Instances () -import Galley.Cassandra.MakeFeature import Galley.Cassandra.Store import Galley.Cassandra.Util import Galley.Effects.TeamFeatureStore qualified as TFS @@ -59,110 +55,16 @@ interpretTeamFeatureStoreToCassandra :: interpretTeamFeatureStoreToCassandra = interpret $ \case TFS.GetDbFeature sing tid -> do logEffect "TeamFeatureStore.GetFeatureConfig" - getDbFeature sing tid + getDbFeatureDyn sing tid TFS.SetDbFeature sing tid feat -> do logEffect "TeamFeatureStore.SetFeatureConfig" - setDbFeature sing tid feat + setDbFeatureDyn sing tid feat TFS.SetFeatureLockStatus sing tid lock -> do logEffect "TeamFeatureStore.SetFeatureLockStatus" - setFeatureLockStatus sing tid (Tagged lock) + setFeatureLockStatusDyn sing tid (Tagged lock) TFS.GetAllDbFeatures tid -> do logEffect "TeamFeatureStore.GetAllTeamFeatures" - getAllDbFeatures tid - TFS.SetMigrationState tid state -> do - logEffect "TeamFeatureStore.SetMigrationState" - setMigrationState tid state - -setMigrationState :: - ( Member (Input ClientState) r, - Member (Embed IO) r - ) => - TeamId -> - TeamFeatureMigrationState -> - Sem r () -setMigrationState tid state = embedClient $ do - retry x5 $ - write cql (params LocalQuorum (state, tid)) - where - cql :: PrepQuery W (TeamFeatureMigrationState, TeamId) () - cql = "UPDATE team_features SET migration_state = ? WHERE team_id = ?" - -getMigrationState :: - ( Member (Input ClientState) r, - Member (Embed IO) r - ) => - TeamId -> - Sem r TeamFeatureMigrationState -getMigrationState tid = embedClient $ do - fromMaybe def . (runIdentity =<<) <$> retry x1 (query1 cql (params LocalQuorum (Identity tid))) - where - cql :: PrepQuery R (Identity TeamId) (Identity (Maybe TeamFeatureMigrationState)) - cql = "SELECT migration_state FROM team_features WHERE team_id = ?" - -getDbFeature :: - ( Member (Input ClientState) r, - Member (Embed IO) r, - Member (Error InternalError) r - ) => - FeatureSingleton cfg -> - TeamId -> - Sem r (DbFeature cfg) -getDbFeature cfg tid = do - migrationState <- getMigrationState tid - case migrationState of - MigrationCompleted -> getDbFeatureDyn cfg tid - _ -> embedClient $ $(featureCases [|fetchFeature|]) cfg tid - -setDbFeature :: - ( Member (Input ClientState) r, - Member (Error InternalError) r, - Member (Embed IO) r - ) => - FeatureSingleton cfg -> - TeamId -> - LockableFeature cfg -> - Sem r () -setDbFeature feature tid cfg = do - migrationState <- getMigrationState tid - case migrationState of - MigrationNotStarted -> embedClient $ $(featureCases [|storeFeature|]) feature tid cfg - MigrationInProgress -> readOnlyError - MigrationCompleted -> setDbFeatureDyn feature tid cfg - -setFeatureLockStatus :: - ( Member (Input ClientState) r, - Member (Error InternalError) r, - Member (Embed IO) r - ) => - FeatureSingleton cfg -> - TeamId -> - Tagged cfg LockStatus -> - Sem r () -setFeatureLockStatus feature tid ls = do - migrationState <- getMigrationState tid - case migrationState of - MigrationNotStarted -> embedClient $ $(featureCases [|storeFeatureLockStatus|]) feature tid ls - MigrationInProgress -> readOnlyError - MigrationCompleted -> setFeatureLockStatusDyn feature tid ls - -getAllDbFeatures :: - ( Member (Input ClientState) r, - Member (Error InternalError) r, - Member (Embed IO) r - ) => - TeamId -> - Sem r (AllFeatures DbFeature) -getAllDbFeatures tid = do - migrationState <- getMigrationState tid - case migrationState of - MigrationCompleted -> getAllDbFeaturesDyn tid - _ -> embedClient $ getAllDbFeaturesLegacy tid - -readOnlyError :: (Member (Error InternalError) r) => Sem r a -readOnlyError = throw (InternalErrorWithDescription "migration in progress") - --------------------------------------------------------------------------------- --- Dynamic features + getAllDbFeaturesDyn tid getDbFeatureDyn :: forall cfg r. diff --git a/services/galley/src/Galley/Effects/TeamFeatureStore.hs b/services/galley/src/Galley/Effects/TeamFeatureStore.hs index 82549dcdaaa..b756ff9281f 100644 --- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs +++ b/services/galley/src/Galley/Effects/TeamFeatureStore.hs @@ -40,7 +40,6 @@ data TeamFeatureStore m a where GetAllDbFeatures :: TeamId -> TeamFeatureStore m (AllFeatures DbFeature) - SetMigrationState :: TeamId -> TeamFeatureMigrationState -> TeamFeatureStore m () getDbFeature :: (Member TeamFeatureStore r, IsFeatureConfig cfg) => @@ -66,6 +65,3 @@ setFeatureLockStatus tid lockStatus = getAllDbFeatures :: (Member TeamFeatureStore r) => TeamId -> Sem r (AllFeatures DbFeature) getAllDbFeatures tid = send (GetAllDbFeatures tid) - -setMigrationState :: (Member TeamFeatureStore r) => TeamId -> TeamFeatureMigrationState -> Sem r () -setMigrationState tid state = send (SetMigrationState tid state) From d152a297266d96c81b322ad952a950ab5d904baf Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 21 Feb 2025 13:48:53 +0100 Subject: [PATCH 24/25] Remove old feature tests --- integration/test/API/GalleyInternal.hs | 8 +- integration/test/Test/FeatureFlags.hs | 21 ++--- integration/test/Test/FeatureFlags/AppLock.hs | 19 ++-- .../Test/FeatureFlags/ConferenceCalling.hs | 12 --- .../Test/FeatureFlags/DigitalSignatures.hs | 9 +- .../Test/FeatureFlags/DomainRegistration.hs | 9 +- .../EnforceFileDownloadLocation.hs | 20 ++-- .../test/Test/FeatureFlags/FileSharing.hs | 9 +- .../test/Test/FeatureFlags/GuestLinks.hs | 19 +--- .../test/Test/FeatureFlags/Initialisation.hs | 6 +- .../test/Test/FeatureFlags/LegalHold.hs | 21 ++--- integration/test/Test/FeatureFlags/Mls.hs | 28 ++---- .../test/Test/FeatureFlags/MlsE2EId.hs | 29 +++--- .../test/Test/FeatureFlags/MlsMigration.hs | 10 +- .../FeatureFlags/OutlookCalIntegration.hs | 9 +- integration/test/Test/FeatureFlags/SSO.hs | 10 +- .../FeatureFlags/SearchVisibilityAvailable.hs | 14 ++- .../FeatureFlags/SearchVisibilityInbound.hs | 5 +- .../Test/FeatureFlags/SelfDeletingMessages.hs | 19 ++-- .../SndFactorPasswordChallenge.hs | 11 +-- integration/test/Test/FeatureFlags/Util.hs | 91 +------------------ .../Test/FeatureFlags/ValidateSAMLEmails.hs | 11 +-- 22 files changed, 117 insertions(+), 273 deletions(-) diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index 45ada18cd11..4fee51bf960 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -47,14 +47,10 @@ setTeamFeatureStatus domain team featureName status = do setTeamFeatureLockStatus :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> App () setTeamFeatureLockStatus domain team featureName status = do - bindResponse (setTeamFeatureLockStatusResponse domain team featureName status) $ \res -> - res.status `shouldMatchInt` 200 - -setTeamFeatureLockStatusResponse :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> App Response -setTeamFeatureLockStatusResponse domain team featureName status = do tid <- asString team req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName, status] - submit "PUT" $ req + bindResponse (submit "PUT" $ req) $ \res -> + res.status `shouldMatchInt` 200 getFederationStatus :: ( HasCallStack, diff --git a/integration/test/Test/FeatureFlags.hs b/integration/test/Test/FeatureFlags.hs index 0e2d9c59f0e..e1ecdae4da2 100644 --- a/integration/test/Test/FeatureFlags.hs +++ b/integration/test/Test/FeatureFlags.hs @@ -29,11 +29,11 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testLimitedEventFanout :: (HasCallStack) => FeatureTable -> App () -testLimitedEventFanout ft = do +testLimitedEventFanout :: (HasCallStack) => App () +testLimitedEventFanout = do let featureName = "limitedEventFanout" (_alice, team, _) <- createTeam OwnDomain 1 - updateMigrationState OwnDomain team ft + -- getTeamFeatureStatus OwnDomain team "limitedEventFanout" "enabled" bindResponse (Internal.getTeamFeature OwnDomain team featureName) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "disabled" @@ -44,10 +44,9 @@ testLimitedEventFanout ft = do -- | Call 'GET /teams/:tid/features' and 'GET /feature-configs', and check if all -- features are there. -testAllFeatures :: (HasCallStack) => FeatureTable -> App () -testAllFeatures ft = do +testAllFeatures :: (HasCallStack) => App () +testAllFeatures = do (_, tid, m : _) <- createTeam OwnDomain 2 - updateMigrationState OwnDomain tid ft bindResponse (Public.getTeamFeatures m tid) $ \resp -> do resp.status `shouldMatchInt` 200 defAllFeatures `shouldMatch` resp.json @@ -71,10 +70,9 @@ testAllFeatures ft = do resp.status `shouldMatchInt` 200 defAllFeatures `shouldMatch` resp.json -testFeatureConfigConsistency :: (HasCallStack) => FeatureTable -> App () -testFeatureConfigConsistency ft = do +testFeatureConfigConsistency :: (HasCallStack) => App () +testFeatureConfigConsistency = do (_, tid, m : _) <- createTeam OwnDomain 2 - updateMigrationState OwnDomain tid ft allFeaturesRes <- Public.getFeatureConfigs m >>= parseObjectKeys @@ -90,10 +88,9 @@ testFeatureConfigConsistency ft = do (A.Object hm) -> pure (Set.fromList . map (show . A.toText) . KM.keys $ hm) x -> assertFailure ("JSON was not an object, but " <> show x) -testNonMemberAccess :: (HasCallStack) => FeatureTable -> Feature -> App () -testNonMemberAccess ft (Feature featureName) = do +testNonMemberAccess :: (HasCallStack) => Feature -> App () +testNonMemberAccess (Feature featureName) = do (_, tid, _) <- createTeam OwnDomain 0 - updateMigrationState OwnDomain tid ft nonMember <- randomUser OwnDomain def Public.getTeamFeature nonMember tid featureName >>= assertForbidden diff --git a/integration/test/Test/FeatureFlags/AppLock.hs b/integration/test/Test/FeatureFlags/AppLock.hs index bd6d5be79f4..f031403a98d 100644 --- a/integration/test/Test/FeatureFlags/AppLock.hs +++ b/integration/test/Test/FeatureFlags/AppLock.hs @@ -4,15 +4,15 @@ import qualified Data.Aeson as A import Test.FeatureFlags.Util import Testlib.Prelude -testPatchAppLock :: (HasCallStack) => FeatureTable -> App () -testPatchAppLock table = do - checkPatchWithTable table OwnDomain "appLock" +testPatchAppLock :: (HasCallStack) => App () +testPatchAppLock = do + checkPatch OwnDomain "appLock" $ object ["lockStatus" .= "locked"] - checkPatchWithTable table OwnDomain "appLock" + checkPatch OwnDomain "appLock" $ object ["status" .= "disabled"] - checkPatchWithTable table OwnDomain "appLock" + checkPatch OwnDomain "appLock" $ object ["lockStatus" .= "locked", "status" .= "disabled"] - checkPatchWithTable table OwnDomain "appLock" + checkPatch OwnDomain "appLock" $ object [ "lockStatus" .= "unlocked", "config" @@ -21,7 +21,7 @@ testPatchAppLock table = do "inactivityTimeoutSecs" .= A.Number 120 ] ] - checkPatchWithTable table OwnDomain "appLock" + checkPatch OwnDomain "appLock" $ object [ "config" .= object @@ -29,8 +29,3 @@ testPatchAppLock table = do "inactivityTimeoutSecs" .= A.Number 240 ] ] - -testPatchAppLockReadOnly :: (HasCallStack) => App () -testPatchAppLockReadOnly = do - checkPatchReadOnly OwnDomain "appLock" - $ object ["lockStatus" .= "locked"] diff --git a/integration/test/Test/FeatureFlags/ConferenceCalling.hs b/integration/test/Test/FeatureFlags/ConferenceCalling.hs index 34e80ac3d0b..30cc5621bcc 100644 --- a/integration/test/Test/FeatureFlags/ConferenceCalling.hs +++ b/integration/test/Test/FeatureFlags/ConferenceCalling.hs @@ -24,15 +24,3 @@ testConferenceCalling access = do & addUpdate (confCalling def {sft = toJSON True}) & addUpdate (confCalling def {sft = toJSON False}) & addInvalidUpdate (confCalling def {sft = toJSON (0 :: Int)}) - -testPatchConferenceCallingReadOnly :: (HasCallStack) => App () -testPatchConferenceCallingReadOnly = do - checkPatchReadOnly OwnDomain "conferenceCalling" - $ object ["lockStatus" .= "locked"] - -testConferenceCallingReadOnlyDuringMigration :: (HasCallStack) => APIAccess -> App () -testConferenceCallingReadOnlyDuringMigration access = do - runFeatureTestsReadOnly OwnDomain access - $ mkFeatureTests "conferenceCalling" - & addUpdate (confCalling def {sft = toJSON True}) - & addUpdate (confCalling def {sft = toJSON False}) diff --git a/integration/test/Test/FeatureFlags/DigitalSignatures.hs b/integration/test/Test/FeatureFlags/DigitalSignatures.hs index 946b0e76cf0..0a00bc33926 100644 --- a/integration/test/Test/FeatureFlags/DigitalSignatures.hs +++ b/integration/test/Test/FeatureFlags/DigitalSignatures.hs @@ -4,13 +4,12 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchDigitalSignatures :: (HasCallStack) => FeatureTable -> App () -testPatchDigitalSignatures table = checkPatchWithTable table OwnDomain "digitalSignatures" enabled +testPatchDigitalSignatures :: (HasCallStack) => App () +testPatchDigitalSignatures = checkPatch OwnDomain "digitalSignatures" enabled -testDigitalSignaturesInternal :: (HasCallStack) => FeatureTable -> App () -testDigitalSignaturesInternal table = do +testDigitalSignaturesInternal :: (HasCallStack) => App () +testDigitalSignaturesInternal = do (alice, tid, _) <- createTeam OwnDomain 0 - updateMigrationState OwnDomain tid table withWebSocket alice $ \ws -> do setFlag InternalAPI ws tid "digitalSignatures" disabled setFlag InternalAPI ws tid "digitalSignatures" enabled diff --git a/integration/test/Test/FeatureFlags/DomainRegistration.hs b/integration/test/Test/FeatureFlags/DomainRegistration.hs index dd342677ead..8ff0d3a1404 100644 --- a/integration/test/Test/FeatureFlags/DomainRegistration.hs +++ b/integration/test/Test/FeatureFlags/DomainRegistration.hs @@ -5,13 +5,12 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchDomainRegistration :: (HasCallStack) => FeatureTable -> App () -testPatchDomainRegistration table = checkPatchWithTable table OwnDomain "domainRegistration" enabled +testPatchDomainRegistration :: (HasCallStack) => App () +testPatchDomainRegistration = checkPatch OwnDomain "domainRegistration" enabled -testDomainRegistrationInternal :: (HasCallStack) => FeatureTable -> App () -testDomainRegistrationInternal table = do +testDomainRegistrationInternal :: (HasCallStack) => App () +testDomainRegistrationInternal = do (alice, tid, _) <- createTeam OwnDomain 0 - updateMigrationState OwnDomain tid table Internal.setTeamFeatureLockStatus alice tid "domainRegistration" "unlocked" withWebSocket alice $ \ws -> do setFlag InternalAPI ws tid "domainRegistration" enabled diff --git a/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs b/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs index c61da4710c9..9bb1a608b4c 100644 --- a/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs +++ b/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs @@ -5,22 +5,21 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchEnforceFileDownloadLocation :: (HasCallStack) => FeatureTable -> App () -testPatchEnforceFileDownloadLocation table = do - checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" +testPatchEnforceFileDownloadLocation :: (HasCallStack) => App () +testPatchEnforceFileDownloadLocation = do + checkPatch OwnDomain "enforceFileDownloadLocation" $ object ["lockStatus" .= "unlocked"] - checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" + checkPatch OwnDomain "enforceFileDownloadLocation" $ object ["status" .= "enabled"] - checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" + checkPatch OwnDomain "enforceFileDownloadLocation" $ object ["lockStatus" .= "unlocked", "status" .= "enabled"] - checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" + checkPatch OwnDomain "enforceFileDownloadLocation" $ object ["lockStatus" .= "locked", "config" .= object []] - checkPatchWithTable table OwnDomain "enforceFileDownloadLocation" + checkPatch OwnDomain "enforceFileDownloadLocation" $ object ["config" .= object ["enforcedDownloadLocation" .= "/tmp"]] do (user, tid, _) <- createTeam OwnDomain 0 - updateMigrationState OwnDomain tid table bindResponse ( Internal.patchTeamFeature user @@ -32,8 +31,8 @@ testPatchEnforceFileDownloadLocation table = do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "empty-download-location" -testEnforceDownloadLocation :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testEnforceDownloadLocation table access = do +testEnforceDownloadLocation :: (HasCallStack) => APIAccess -> App () +testEnforceDownloadLocation access = do mkFeatureTests "enforceFileDownloadLocation" & addUpdate @@ -53,5 +52,4 @@ testEnforceDownloadLocation table access = do ] ] ) - & setTable table & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/FileSharing.hs b/integration/test/Test/FeatureFlags/FileSharing.hs index eae3538511d..7cc761e64ef 100644 --- a/integration/test/Test/FeatureFlags/FileSharing.hs +++ b/integration/test/Test/FeatureFlags/FileSharing.hs @@ -3,13 +3,12 @@ module Test.FeatureFlags.FileSharing where import Test.FeatureFlags.Util import Testlib.Prelude -testPatchFileSharing :: (HasCallStack) => FeatureTable -> App () -testPatchFileSharing table = checkPatchWithTable table OwnDomain "fileSharing" disabled +testPatchFileSharing :: (HasCallStack) => App () +testPatchFileSharing = checkPatch OwnDomain "fileSharing" disabled -testFileSharing :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testFileSharing table access = +testFileSharing :: (HasCallStack) => APIAccess -> App () +testFileSharing access = mkFeatureTests "fileSharing" & addUpdate disabled & addUpdate enabled - & setTable table & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/GuestLinks.hs b/integration/test/Test/FeatureFlags/GuestLinks.hs index 9c1d513779f..0c0c84ae387 100644 --- a/integration/test/Test/FeatureFlags/GuestLinks.hs +++ b/integration/test/Test/FeatureFlags/GuestLinks.hs @@ -3,23 +3,12 @@ module Test.FeatureFlags.GuestLinks where import Test.FeatureFlags.Util import Testlib.Prelude -testConversationGuestLinks :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testConversationGuestLinks table access = +testConversationGuestLinks :: (HasCallStack) => APIAccess -> App () +testConversationGuestLinks access = mkFeatureTests "conversationGuestLinks" & addUpdate disabled & addUpdate enabled - & setTable table & runFeatureTests OwnDomain access -testPatchGuestLinks :: (HasCallStack) => FeatureTable -> App () -testPatchGuestLinks table = checkPatchWithTable table OwnDomain "conversationGuestLinks" disabled - -testConversationGuestLinksReadOnly :: (HasCallStack) => APIAccess -> App () -testConversationGuestLinksReadOnly access = - runFeatureTestsReadOnly OwnDomain access - $ mkFeatureTests "conversationGuestLinks" - & addUpdate disabled - & addUpdate enabled - -testPatchGuestLinksReadOnly :: (HasCallStack) => App () -testPatchGuestLinksReadOnly = checkPatchReadOnly OwnDomain "conversationGuestLinks" disabled +testPatchGuestLinks :: (HasCallStack) => App () +testPatchGuestLinks = checkPatch OwnDomain "conversationGuestLinks" disabled diff --git a/integration/test/Test/FeatureFlags/Initialisation.hs b/integration/test/Test/FeatureFlags/Initialisation.hs index ef62bb95661..ac84a57ac91 100644 --- a/integration/test/Test/FeatureFlags/Initialisation.hs +++ b/integration/test/Test/FeatureFlags/Initialisation.hs @@ -5,12 +5,11 @@ import Control.Monad.Codensity import Control.Monad.Extra import Control.Monad.Reader import SetupHelpers -import Test.FeatureFlags.Util import Testlib.Prelude import Testlib.ResourcePool -testMLSInitialisation :: (HasCallStack) => FeatureTable -> App () -testMLSInitialisation table = do +testMLSInitialisation :: (HasCallStack) => App () +testMLSInitialisation = do let override = def { galleyCfg = @@ -43,7 +42,6 @@ testMLSInitialisation table = do (alice, tid, _) <- createTeam domain 0 feat <- getTeamFeature alice tid "mls" >>= getJSON 200 feat %. "config.defaultProtocol" `shouldMatch` "proteus" - updateMigrationState domain tid table pure (alice, tid) lift $ lowerCodensity do diff --git a/integration/test/Test/FeatureFlags/LegalHold.hs b/integration/test/Test/FeatureFlags/LegalHold.hs index fc523029826..45f099aef5c 100644 --- a/integration/test/Test/FeatureFlags/LegalHold.hs +++ b/integration/test/Test/FeatureFlags/LegalHold.hs @@ -9,8 +9,8 @@ import Test.FeatureFlags.Util import Testlib.Prelude import Testlib.ResourcePool (acquireResources) -testLegalholdDisabledByDefault :: (HasCallStack) => FeatureTable -> App () -testLegalholdDisabledByDefault table = do +testLegalholdDisabledByDefault :: (HasCallStack) => App () +testLegalholdDisabledByDefault = do let put uid tid st = Internal.setTeamFeatureConfig uid tid "legalhold" (object ["status" .= st]) >>= assertSuccess let patch uid tid st = Internal.setTeamFeatureStatus uid tid "legalhold" st >>= assertSuccess forM_ [put, patch] $ \setFeatureStatus -> do @@ -18,7 +18,6 @@ testLegalholdDisabledByDefault table = do def {galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default"} $ \domain -> do (owner, tid, m : _) <- createTeam domain 2 - updateMigrationState domain tid table nonMember <- randomUser domain def assertForbidden =<< Public.getTeamFeature nonMember tid "legalhold" -- Test default @@ -30,8 +29,8 @@ testLegalholdDisabledByDefault table = do checkFeature "legalhold" owner tid disabled -- always disabled -testLegalholdDisabledPermanently :: (HasCallStack) => FeatureTable -> App () -testLegalholdDisabledPermanently table = do +testLegalholdDisabledPermanently :: (HasCallStack) => App () +testLegalholdDisabledPermanently = do let cfgLhDisabledPermanently = def { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-permanently" @@ -47,7 +46,6 @@ testLegalholdDisabledPermanently table = do -- Happy case: DB has no config for the team runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do (owner, tid, _) <- createTeam domain 1 - updateMigrationState domain tid table checkFeature "legalhold" owner tid disabled assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "enabled"]) @@ -56,7 +54,6 @@ testLegalholdDisabledPermanently table = do -- changed to disabled-permanently (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do (owner, tid, _) <- createTeam domain 1 - updateMigrationState domain tid table checkFeature "legalhold" owner tid disabled assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" checkFeature "legalhold" owner tid enabled @@ -66,8 +63,8 @@ testLegalholdDisabledPermanently table = do checkFeature "legalhold" owner tid disabled -- enabled if team is allow listed, disabled in any other case -testLegalholdWhitelistTeamsAndImplicitConsent :: (HasCallStack) => FeatureTable -> App () -testLegalholdWhitelistTeamsAndImplicitConsent table = do +testLegalholdWhitelistTeamsAndImplicitConsent :: (HasCallStack) => App () +testLegalholdWhitelistTeamsAndImplicitConsent = do let cfgLhWhitelistTeamsAndImplicitConsent = def { galleyCfg = setField "settings.featureFlags.legalhold" "whitelist-teams-and-implicit-consent" @@ -83,7 +80,6 @@ testLegalholdWhitelistTeamsAndImplicitConsent table = do -- Happy case: DB has no config for the team (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do (owner, tid, _) <- createTeam domain 1 - updateMigrationState domain tid table checkFeature "legalhold" owner tid disabled Internal.legalholdWhitelistTeam tid owner >>= assertSuccess checkFeature "legalhold" owner tid enabled @@ -105,8 +101,8 @@ testLegalholdWhitelistTeamsAndImplicitConsent table = do runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do checkFeature "legalhold" owner tid enabled -testExposeInvitationURLsToTeamAdminConfig :: (HasCallStack) => FeatureTable -> App () -testExposeInvitationURLsToTeamAdminConfig table = do +testExposeInvitationURLsToTeamAdminConfig :: (HasCallStack) => App () +testExposeInvitationURLsToTeamAdminConfig = do let cfgExposeInvitationURLsTeamAllowlist tids = def { galleyCfg = setField "settings.exposeInvitationURLsTeamAllowlist" tids @@ -118,7 +114,6 @@ testExposeInvitationURLsToTeamAdminConfig table = do testNoAllowlistEntry :: (HasCallStack) => App (Value, String) testNoAllowlistEntry = runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist ([] :: [String])) $ \_ -> do (owner, tid, _) <- createTeam domain 1 - updateMigrationState domain tid table checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked -- here we get a response with HTTP status 200 and feature status unchanged (disabled), which we find weird, but we're just testing the current behavior -- a team that is not in the allow list cannot enable the feature, it will always be disabled and locked diff --git a/integration/test/Test/FeatureFlags/Mls.hs b/integration/test/Test/FeatureFlags/Mls.hs index 34b042dfcd7..73cc96eaf12 100644 --- a/integration/test/Test/FeatureFlags/Mls.hs +++ b/integration/test/Test/FeatureFlags/Mls.hs @@ -4,8 +4,8 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testMls :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testMls table access = +testMls :: (HasCallStack) => APIAccess -> App () +testMls access = do user <- randomUser OwnDomain def uid <- asString $ user %. "id" @@ -13,11 +13,10 @@ testMls table access = & addUpdate (mls1 uid) & addUpdate mls2 & addInvalidUpdate mlsInvalidConfig - & setTable table & runFeatureTests OwnDomain access -testMlsPatch :: (HasCallStack) => FeatureTable -> App () -testMlsPatch table = do +testMlsPatch :: (HasCallStack) => App () +testMlsPatch = do mlsMigrationDefaultConfig <- defAllFeatures %. "mlsMigration.config" withModifiedBackend def @@ -32,11 +31,11 @@ testMlsPatch table = do ) } $ \domain -> do - checkPatchWithTable table domain "mls" $ object ["lockStatus" .= "locked"] - checkPatchWithTable table domain "mls" $ object ["status" .= "enabled"] - checkPatchWithTable table domain "mls" + checkPatch domain "mls" $ object ["lockStatus" .= "locked"] + checkPatch domain "mls" $ object ["status" .= "enabled"] + checkPatch domain "mls" $ object ["lockStatus" .= "locked", "status" .= "enabled"] - checkPatchWithTable table domain "mls" + checkPatch domain "mls" $ object [ "status" .= "enabled", "config" @@ -48,7 +47,7 @@ testMlsPatch table = do "defaultCipherSuite" .= toJSON (1 :: Int) ] ] - checkPatchWithTable table domain "mls" + checkPatch domain "mls" $ object [ "config" .= object @@ -60,15 +59,6 @@ testMlsPatch table = do ] ] -testMlsReadOnly :: (HasCallStack) => APIAccess -> App () -testMlsReadOnly access = - runFeatureTestsReadOnly OwnDomain access - $ mkFeatureTests "mls" - & addUpdate mls2 - -testPatchMlsReadOnly :: (HasCallStack) => App () -testPatchMlsReadOnly = checkPatchReadOnly OwnDomain "mls" mls2 - mls1 :: String -> Value mls1 uid = object diff --git a/integration/test/Test/FeatureFlags/MlsE2EId.hs b/integration/test/Test/FeatureFlags/MlsE2EId.hs index 2a5c06c9310..dee32f94be2 100644 --- a/integration/test/Test/FeatureFlags/MlsE2EId.hs +++ b/integration/test/Test/FeatureFlags/MlsE2EId.hs @@ -18,8 +18,8 @@ mlsE2EId1 = ] ] -testMLSE2EId :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testMLSE2EId table access = do +testMLSE2EId :: (HasCallStack) => APIAccess -> App () +testMLSE2EId access = do invalid <- mlsE2EId1 & if (access == InternalAPI) @@ -34,16 +34,15 @@ testMLSE2EId table access = do & addUpdate mlsE2EId1 & addUpdate mlsE2EId2 & addInvalidUpdate invalid - & setTable table & runFeatureTests OwnDomain access -testPatchE2EId :: (HasCallStack) => FeatureTable -> App () -testPatchE2EId table = do - checkPatchWithTable table OwnDomain "mlsE2EId" (object ["lockStatus" .= "locked"]) - checkPatchWithTable table OwnDomain "mlsE2EId" (object ["status" .= "enabled"]) - checkPatchWithTable table OwnDomain "mlsE2EId" +testPatchE2EId :: (HasCallStack) => App () +testPatchE2EId = do + checkPatch OwnDomain "mlsE2EId" (object ["lockStatus" .= "locked"]) + checkPatch OwnDomain "mlsE2EId" (object ["status" .= "enabled"]) + checkPatch OwnDomain "mlsE2EId" $ object ["lockStatus" .= "locked", "status" .= "enabled"] - checkPatchWithTable table OwnDomain "mlsE2EId" + checkPatch OwnDomain "mlsE2EId" $ object [ "lockStatus" .= "unlocked", "config" @@ -54,7 +53,7 @@ testPatchE2EId table = do ] ] - checkPatchWithTable table OwnDomain "mlsE2EId" + checkPatch OwnDomain "mlsE2EId" $ object [ "config" .= object @@ -64,10 +63,9 @@ testPatchE2EId table = do ] ] -testMlsE2EConfigCrlProxyRequired :: (HasCallStack) => FeatureTable -> App () -testMlsE2EConfigCrlProxyRequired table = do +testMlsE2EConfigCrlProxyRequired :: (HasCallStack) => App () +testMlsE2EConfigCrlProxyRequired = do (owner, tid, _) <- createTeam OwnDomain 1 - updateMigrationState OwnDomain tid table let configWithoutCrlProxy = object [ "config" @@ -97,10 +95,9 @@ testMlsE2EConfigCrlProxyRequired table = do expectedResponse <- configWithCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited" checkFeature "mlsE2EId" owner tid expectedResponse -testMlsE2EConfigCrlProxyNotRequiredInV5 :: (HasCallStack) => FeatureTable -> App () -testMlsE2EConfigCrlProxyNotRequiredInV5 table = do +testMlsE2EConfigCrlProxyNotRequiredInV5 :: (HasCallStack) => App () +testMlsE2EConfigCrlProxyNotRequiredInV5 = do (owner, tid, _) <- createTeam OwnDomain 1 - updateMigrationState OwnDomain tid table let configWithoutCrlProxy = object [ "config" diff --git a/integration/test/Test/FeatureFlags/MlsMigration.hs b/integration/test/Test/FeatureFlags/MlsMigration.hs index 8b5941e6781..bac309fa5bb 100644 --- a/integration/test/Test/FeatureFlags/MlsMigration.hs +++ b/integration/test/Test/FeatureFlags/MlsMigration.hs @@ -7,11 +7,10 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testMlsMigration :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testMlsMigration table access = do +testMlsMigration :: (HasCallStack) => APIAccess -> App () +testMlsMigration access = do -- first we have to enable mls (owner, tid, _) <- createTeam OwnDomain 0 - updateMigrationState OwnDomain tid table void $ Public.setTeamFeatureConfig owner tid "mls" mlsEnable >>= getJSON 200 mkFeatureTests "mlsMigration" & addUpdate mlsMigrationConfig1 @@ -19,15 +18,14 @@ testMlsMigration table access = do & setOwner owner >>= runFeatureTests OwnDomain access -testMlsMigrationDefaults :: (HasCallStack) => FeatureTable -> App () -testMlsMigrationDefaults table = do +testMlsMigrationDefaults :: (HasCallStack) => App () +testMlsMigrationDefaults = do withModifiedBackend def { galleyCfg = setField "settings.featureFlags.mlsMigration.defaults.lockStatus" "unlocked" } $ \domain -> do (owner, tid, _) <- createTeam domain 0 - updateMigrationState OwnDomain tid table void $ Internal.patchTeamFeature owner tid "mls" (object ["status" .= "enabled"]) >>= getJSON 200 diff --git a/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs b/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs index 06c9aba1b34..8db8464a8d1 100644 --- a/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs +++ b/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs @@ -3,13 +3,12 @@ module Test.FeatureFlags.OutlookCalIntegration where import Test.FeatureFlags.Util import Testlib.Prelude -testPatchOutlookCalIntegration :: (HasCallStack) => FeatureTable -> App () -testPatchOutlookCalIntegration table = checkPatchWithTable table OwnDomain "outlookCalIntegration" enabled +testPatchOutlookCalIntegration :: (HasCallStack) => App () +testPatchOutlookCalIntegration = checkPatch OwnDomain "outlookCalIntegration" enabled -testOutlookCalIntegration :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testOutlookCalIntegration table access = +testOutlookCalIntegration :: (HasCallStack) => APIAccess -> App () +testOutlookCalIntegration access = mkFeatureTests "outlookCalIntegration" & addUpdate enabled & addUpdate disabled - & setTable table & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/SSO.hs b/integration/test/Test/FeatureFlags/SSO.hs index 2ddcef57877..7b633ddcb10 100644 --- a/integration/test/Test/FeatureFlags/SSO.hs +++ b/integration/test/Test/FeatureFlags/SSO.hs @@ -6,8 +6,8 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testSSODisabledByDefault :: (HasCallStack) => FeatureTable -> App () -testSSODisabledByDefault table = do +testSSODisabledByDefault :: (HasCallStack) => App () +testSSODisabledByDefault = do let put uid tid = Internal.setTeamFeatureConfig uid tid "sso" (object ["status" .= "enabled"]) >>= assertSuccess let patch uid tid = Internal.setTeamFeatureStatus uid tid "sso" "enabled" >>= assertSuccess forM_ [put, patch] $ \enableFeature -> do @@ -15,7 +15,6 @@ testSSODisabledByDefault table = do def {galleyCfg = setField "settings.featureFlags.sso" "disabled-by-default"} $ \domain -> do (owner, tid, m : _) <- createTeam domain 2 - updateMigrationState domain tid table nonMember <- randomUser domain def assertForbidden =<< Public.getTeamFeature nonMember tid "sso" -- Test default @@ -24,13 +23,12 @@ testSSODisabledByDefault table = do enableFeature owner tid checkFeature "sso" owner tid enabled -testSSOEnabledByDefault :: (HasCallStack) => FeatureTable -> App () -testSSOEnabledByDefault table = do +testSSOEnabledByDefault :: (HasCallStack) => App () +testSSOEnabledByDefault = do withModifiedBackend def {galleyCfg = setField "settings.featureFlags.sso" "enabled-by-default"} $ \domain -> do (owner, tid, _m : _) <- createTeam domain 2 - updateMigrationState domain tid table nonMember <- randomUser domain def assertForbidden =<< Public.getTeamFeature nonMember tid "sso" checkFeature "sso" owner tid enabled diff --git a/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs b/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs index a21828d7966..a2ce39cd44e 100644 --- a/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs +++ b/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs @@ -6,14 +6,13 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchSearchVisibility :: (HasCallStack) => FeatureTable -> App () -testPatchSearchVisibility table = checkPatchWithTable table OwnDomain "searchVisibility" enabled +testPatchSearchVisibility :: (HasCallStack) => App () +testPatchSearchVisibility = checkPatch OwnDomain "searchVisibility" enabled -testSearchVisibilityDisabledByDefault :: (HasCallStack) => FeatureTable -> App () -testSearchVisibilityDisabledByDefault table = do +testSearchVisibilityDisabledByDefault :: (HasCallStack) => App () +testSearchVisibilityDisabledByDefault = do withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "disabled-by-default"} $ \domain -> do (owner, tid, m : _) <- createTeam domain 2 - updateMigrationState domain tid table -- Test default checkFeature "searchVisibility" m tid disabled assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" @@ -21,11 +20,10 @@ testSearchVisibilityDisabledByDefault table = do assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" checkFeature "searchVisibility" owner tid disabled -testSearchVisibilityEnabledByDefault :: (HasCallStack) => FeatureTable -> App () -testSearchVisibilityEnabledByDefault table = do +testSearchVisibilityEnabledByDefault :: (HasCallStack) => App () +testSearchVisibilityEnabledByDefault = do withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} $ \domain -> do (owner, tid, m : _) <- createTeam domain 2 - updateMigrationState domain tid table nonMember <- randomUser domain def assertForbidden =<< Public.getTeamFeature nonMember tid "searchVisibility" -- Test default diff --git a/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs index 2919d19010b..c1ef7a5d3ca 100644 --- a/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs +++ b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs @@ -5,11 +5,10 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testSearchVisibilityInboundInternal :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testSearchVisibilityInboundInternal table access = do +testSearchVisibilityInboundInternal :: (HasCallStack) => APIAccess -> App () +testSearchVisibilityInboundInternal access = do let featureName = "searchVisibilityInbound" (alice, tid, _) <- createTeam OwnDomain 2 - updateMigrationState OwnDomain tid table eve <- randomUser OwnDomain def assertForbidden =<< Public.getTeamFeature eve tid featureName checkFeature featureName alice tid disabled diff --git a/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs b/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs index ee3954d02f4..019bed20341 100644 --- a/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs +++ b/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs @@ -13,24 +13,23 @@ feature ps timeout = ] ) -testSelfDeletingMessages :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testSelfDeletingMessages table access = +testSelfDeletingMessages :: (HasCallStack) => APIAccess -> App () +testSelfDeletingMessages access = mkFeatureTests "selfDeletingMessages" & addUpdate (feature ["status" .= "disabled"] (0 :: Int)) & addUpdate (feature ["status" .= "enabled"] (30 :: Int)) & addInvalidUpdate (feature ["status" .= "enabled"] "") - & setTable table & runFeatureTests OwnDomain access -testPatchSelfDeletingMessages :: (HasCallStack) => FeatureTable -> App () -testPatchSelfDeletingMessages table = do - checkPatchWithTable table OwnDomain "selfDeletingMessages" +testPatchSelfDeletingMessages :: (HasCallStack) => App () +testPatchSelfDeletingMessages = do + checkPatch OwnDomain "selfDeletingMessages" $ object ["lockStatus" .= "locked"] - checkPatchWithTable table OwnDomain "selfDeletingMessages" + checkPatch OwnDomain "selfDeletingMessages" $ object ["status" .= "disabled"] - checkPatchWithTable table OwnDomain "selfDeletingMessages" + checkPatch OwnDomain "selfDeletingMessages" $ object ["lockStatus" .= "locked", "status" .= "disabled"] - checkPatchWithTable table OwnDomain "selfDeletingMessages" + checkPatch OwnDomain "selfDeletingMessages" $ object ["lockStatus" .= "unlocked", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]] - checkPatchWithTable table OwnDomain "selfDeletingMessages" + checkPatch OwnDomain "selfDeletingMessages" $ object ["config" .= object ["enforcedTimeoutSeconds" .= A.Number 60]] diff --git a/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs b/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs index 8fb976ec8d1..7acc3621f4e 100644 --- a/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs +++ b/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs @@ -3,15 +3,14 @@ module Test.FeatureFlags.SndFactorPasswordChallenge where import Test.FeatureFlags.Util import Testlib.Prelude -testPatchSndFactorPasswordChallenge :: (HasCallStack) => FeatureTable -> App () -testPatchSndFactorPasswordChallenge table = - checkPatchWithTable table OwnDomain "sndFactorPasswordChallenge" enabled +testPatchSndFactorPasswordChallenge :: (HasCallStack) => App () +testPatchSndFactorPasswordChallenge = + checkPatch OwnDomain "sndFactorPasswordChallenge" enabled -testSndFactorPasswordChallenge :: (HasCallStack) => FeatureTable -> APIAccess -> App () -testSndFactorPasswordChallenge table access = +testSndFactorPasswordChallenge :: (HasCallStack) => APIAccess -> App () +testSndFactorPasswordChallenge access = do mkFeatureTests "sndFactorPasswordChallenge" & addUpdate enabled & addUpdate disabled - & setTable table & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 38944d0f1c1..821b9220146 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -218,19 +218,8 @@ checkPatch :: String -> Value -> App () -checkPatch = checkPatchWithTable FeatureTableLegacy - -checkPatchWithTable :: - (HasCallStack, MakesValue domain) => - FeatureTable -> - domain -> - String -> - Value -> - App () -checkPatchWithTable table domain featureName patch = do +checkPatch domain featureName patch = do (owner, tid, _) <- createTeam domain 0 - updateMigrationState domain tid table - defFeature <- defAllFeatures %. featureName let valueOrDefault :: String -> App Value @@ -278,12 +267,11 @@ data FeatureTests = FeatureTests -- payload) updates :: [Value], invalidUpdates :: [Value], - owner :: Maybe Value, - table :: FeatureTable + owner :: Maybe Value } mkFeatureTests :: String -> FeatureTests -mkFeatureTests name = FeatureTests name [] [] Nothing FeatureTableLegacy +mkFeatureTests name = FeatureTests name [] [] Nothing addUpdate :: Value -> FeatureTests -> FeatureTests addUpdate up ft = ft {updates = ft.updates <> [up]} @@ -296,9 +284,6 @@ setOwner owner ft = do x <- make owner pure ft {owner = Just x} -setTable :: FeatureTable -> FeatureTests -> FeatureTests -setTable table ft = ft {table = table} - runFeatureTests :: (HasCallStack, MakesValue domain) => domain -> @@ -325,7 +310,6 @@ runFeatureTests domain access ft = do Just owner -> do tid <- owner %. "team" & asString pure (owner, tid) - updateMigrationState domain tid ft.table checkFeature ft.name owner tid defFeature -- lock the feature @@ -361,72 +345,3 @@ runFeatureTests domain access ft = do setField "ttl" "unlimited" =<< setField "lockStatus" "unlocked" update checkFeature ft.name owner tid expected - -data FeatureTable = FeatureTableLegacy | FeatureTableDyn - deriving (Show, Eq, Generic) - -updateMigrationState :: (HasCallStack, MakesValue domain) => domain -> String -> FeatureTable -> App () -updateMigrationState domain tid ft = case ft of - FeatureTableLegacy -> pure () - FeatureTableDyn -> do - Internal.setTeamFeatureMigrationState domain tid "completed" >>= assertSuccess - -runFeatureTestsReadOnly :: - (HasCallStack, MakesValue domain) => - domain -> - APIAccess -> - FeatureTests -> - App () -runFeatureTestsReadOnly domain access ft = do - defFeature <- defAllFeatures %. ft.name - do - user <- randomUser domain def - bindResponse (Public.getFeatureConfigs user) $ \resp -> do - resp.status `shouldMatchInt` 200 - feat <- resp.json %. ft.name - lockStatus <- feat %. "lockStatus" - expected <- setField "lockStatus" lockStatus defFeature - feat `shouldMatch` expected - - (owner, tid, _) <- createTeam domain 0 - - checkFeature ft.name owner tid defFeature - - -- unlock the feature - Internal.setTeamFeatureLockStatus owner tid ft.name "unlocked" - - -- set migration state to in progress - void $ Internal.setTeamFeatureMigrationState domain tid "in_progress" - featureStatus <- Internal.getTeamFeature domain tid ft.name >>= getJSON 200 - - -- locking the feature should not work - Internal.setTeamFeatureLockStatusResponse owner tid ft.name "locked" `bindResponse` assertMigrationInProgress - - -- updates do not work - for_ ft.updates $ \u -> do - setFeature access owner tid ft.name u `bindResponse` assertMigrationInProgress - - checkFeature ft.name owner tid featureStatus - -assertMigrationInProgress :: (HasCallStack) => Response -> App () -assertMigrationInProgress res = do - res.status `shouldMatchInt` 500 - res.json %. "label" `shouldMatch` "internal-error" - res.json %. "message" `shouldMatch` "migration in progress" - -checkPatchReadOnly :: - (HasCallStack, MakesValue domain) => - domain -> - String -> - Value -> - App () -checkPatchReadOnly domain featureName patch = do - (owner, tid, _) <- createTeam domain 0 - void $ Internal.setTeamFeatureMigrationState domain tid "in_progress" - defFeature <- defAllFeatures %. featureName - - checkFeature featureName owner tid defFeature - Internal.patchTeamFeature domain tid featureName patch - `bindResponse` assertMigrationInProgress - - checkFeature featureName owner tid defFeature diff --git a/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs b/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs index fcd945460e8..6177c52be87 100644 --- a/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs +++ b/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs @@ -4,15 +4,14 @@ import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testPatchValidateSAMLEmails :: (HasCallStack) => FeatureTable -> App () -testPatchValidateSAMLEmails table = - checkPatchWithTable table OwnDomain "validateSAMLemails" +testPatchValidateSAMLEmails :: (HasCallStack) => App () +testPatchValidateSAMLEmails = + checkPatch OwnDomain "validateSAMLemails" $ object ["status" .= "disabled"] -testValidateSAMLEmailsInternal :: (HasCallStack) => FeatureTable -> App () -testValidateSAMLEmailsInternal table = do +testValidateSAMLEmailsInternal :: (HasCallStack) => App () +testValidateSAMLEmailsInternal = do (alice, tid, _) <- createTeam OwnDomain 0 - updateMigrationState OwnDomain tid table withWebSocket alice $ \ws -> do setFlag InternalAPI ws tid "validateSAMLemails" disabled setFlag InternalAPI ws tid "validateSAMLemails" enabled From 271b5dda2971222cf85dc0599ee891b0001dc20f Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 25 Feb 2025 08:50:22 +0100 Subject: [PATCH 25/25] Add CHANGELOG entry --- changelog.d/5-internal/cleanup-old-features | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5-internal/cleanup-old-features diff --git a/changelog.d/5-internal/cleanup-old-features b/changelog.d/5-internal/cleanup-old-features new file mode 100644 index 00000000000..954568e4be8 --- /dev/null +++ b/changelog.d/5-internal/cleanup-old-features @@ -0,0 +1 @@ +Remove legacy team feature storage support