diff --git a/changelog.d/2-features/WPB-25314 b/changelog.d/2-features/WPB-25314 new file mode 100644 index 00000000000..af0eb6ee918 --- /dev/null +++ b/changelog.d/2-features/WPB-25314 @@ -0,0 +1 @@ +Added a team feature to configure adminless group prevention. diff --git a/integration/integration.cabal b/integration/integration.cabal index 49fc43bcf5a..b388fbd0bca 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -158,6 +158,7 @@ library Test.FeatureFlags.MlsE2EId Test.FeatureFlags.MlsMigration Test.FeatureFlags.OutlookCalIntegration + Test.FeatureFlags.PreventAdminlessGroups Test.FeatureFlags.RequireExternalEmailVerification Test.FeatureFlags.SearchVisibilityAvailable Test.FeatureFlags.SearchVisibilityInbound diff --git a/integration/test/Test/FeatureFlags/PreventAdminlessGroups.hs b/integration/test/Test/FeatureFlags/PreventAdminlessGroups.hs new file mode 100644 index 00000000000..87ce110593e --- /dev/null +++ b/integration/test/Test/FeatureFlags/PreventAdminlessGroups.hs @@ -0,0 +1,71 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.FeatureFlags.PreventAdminlessGroups where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPreventAdminlessGroups :: (HasCallStack) => APIAccess -> App () +testPreventAdminlessGroups access = + mkFeatureTests "preventAdminlessGroups" + & addUpdate validConfig + & addInvalidUpdate invalidConfig + & runFeatureTests OwnDomain access + +validConfig :: Value +validConfig = + object + [ "status" .= "enabled", + "config" + .= object + [ "promotionStrategy" .= "random", + "deletionTimeout" .= (30 :: Int), + "reminderTimeouts" .= ([15, 20, 25] :: [Int]) + ] + ] + +invalidConfig :: Value +invalidConfig = + object + [ "status" .= "enabled", + "config" + .= object + [ "promotionStrategy" .= "dsdfhjsdf", + "deletionTimeout" .= (30 :: Int), + "reminderTimeouts" .= ([15, 20, 25] :: [Int]) + ] + ] + +testPatchPreventAdminlessGroups :: (HasCallStack) => App () +testPatchPreventAdminlessGroups = do + checkPatch OwnDomain "preventAdminlessGroups" + $ object ["lockStatus" .= "locked"] + checkPatch OwnDomain "preventAdminlessGroups" + $ object ["status" .= "disabled"] + checkPatch OwnDomain "preventAdminlessGroups" + $ object ["lockStatus" .= "locked", "status" .= "disabled"] + checkPatch OwnDomain "preventAdminlessGroups" + $ object + [ "lockStatus" .= "unlocked", + "config" + .= object + [ "promotionStrategy" .= "random", + "deletionTimeout" .= (30 :: Int), + "reminderTimeouts" .= ([15, 20, 25] :: [Int]) + ] + ] diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 64381efc644..295869f48e2 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -242,7 +242,19 @@ defAllFeatures = ] ], "meetings" .= enabled, - "meetingsPremium" .= disabledLocked + "meetingsPremium" .= disabledLocked, + "preventAdminlessGroups" + .= object + [ "lockStatus" .= "locked", + "status" .= "disabled", + "ttl" .= "unlimited", + "config" + .= object + [ "promotionStrategy" .= "alphabetical", + "deletionTimeout" .= (7 :: Int), + "reminderTimeouts" .= ([2, 4, 6] :: [Int]) + ] + ] ] hasExplicitLockStatus :: String -> Bool @@ -323,6 +335,20 @@ defAllConfiguredFeatures = "searchVisibilityInbound" .= defaults disabled', "exposeInvitationURLsToTeamAdmin" .= "expose-invitation-urls-to-team-admin-defaults", "outlookCalIntegration" .= defaults disabledLocked, + "preventAdminlessGroups" + .= defaults + ( object + [ "config" + .= object + [ "deletionTimeout" .= (7 :: Int), + "promotionStrategy" .= "alphabetical", + "reminderTimeouts" .= ([2, 4, 6] :: [Int]) + ], + "lockStatus" .= "locked", + "status" .= "disabled", + "ttl" .= "unlimited" + ] + ), "mlsE2EId" .= defaults ( object 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 a254f170483..01e0f4f1051 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -91,6 +91,7 @@ type IFeatureAPI = :<|> IFeatureStatusLockStatusPut EnforceFileDownloadLocationConfig :<|> IFeatureStatusLockStatusPut DomainRegistrationConfig :<|> IFeatureStatusLockStatusPut ChannelsConfig + :<|> IFeatureStatusLockStatusPut PreventAdminlessGroupsConfig :<|> IFeatureStatusLockStatusPut CellsConfig :<|> IFeatureStatusLockStatusPut ConsumableNotificationsConfig :<|> IFeatureStatusLockStatusPut ChatBubblesConfig diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index 2083e829754..fee909d1351 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -67,6 +67,7 @@ type FeatureAPI = :<|> AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs :<|> FeatureAPIGet DomainRegistrationConfig :<|> FeatureAPIGetPut ChannelsConfig + :<|> FeatureAPIGetPut PreventAdminlessGroupsConfig :<|> FeatureAPIGet CellsConfig :<|> Until 'V14 ::> VersionedFeatureAPIPut "put-CellsConfig@v13" V13 CellsConfig :<|> From 'V14 ::> FeatureAPIPut CellsConfig diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 12185dbcc89..a1463bb7ae1 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -78,6 +78,9 @@ module Wire.API.Team.Feature ChannelsConfig, ChannelsConfigB (..), ChannelPermissions (..), + PreventAdminlessGroupsConfig, + PreventAdminlessGroupsConfigB (..), + PreventAdminlessGroupsPromotionStrategy (..), OutlookCalIntegrationConfig (..), UseProxyOnMobile (..), MlsE2EIdConfigB (..), @@ -274,6 +277,7 @@ data FeatureSingleton cfg where FeatureSingletonLimitedEventFanoutConfig :: FeatureSingleton LimitedEventFanoutConfig FeatureSingletonDomainRegistrationConfig :: FeatureSingleton DomainRegistrationConfig FeatureSingletonChannelsConfig :: FeatureSingleton ChannelsConfig + FeatureSingletonPreventAdminlessGroupsConfig :: FeatureSingleton PreventAdminlessGroupsConfig FeatureSingletonCellsConfig :: FeatureSingleton CellsConfig FeatureSingletonAllowedGlobalOperationsConfig :: FeatureSingleton AllowedGlobalOperationsConfig FeatureSingletonConsumableNotificationsConfig :: FeatureSingleton ConsumableNotificationsConfig @@ -1220,6 +1224,79 @@ instance IsFeatureConfig ChannelsConfig where type FeatureSymbol ChannelsConfig = "channels" featureSingleton = FeatureSingletonChannelsConfig +---------------------------------------------------------------------- +-- PreventAdminlessGroupsConfig + +data PreventAdminlessGroupsPromotionStrategy + = PromotionStrategyAlphabetical + | PromotionStrategyRandom + | PromotionStrategyAll + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema PreventAdminlessGroupsPromotionStrategy + deriving (Arbitrary) via (GenericUniform PreventAdminlessGroupsPromotionStrategy) + +instance ToSchema PreventAdminlessGroupsPromotionStrategy where + schema = + enum @Text $ + mconcat + [ element "alphabetical" PromotionStrategyAlphabetical, + element "random" PromotionStrategyRandom, + element "all" PromotionStrategyAll + ] + +data PreventAdminlessGroupsConfigB t f = PreventAdminlessGroupsConfig + { promotionStrategy :: Wear t f PreventAdminlessGroupsPromotionStrategy, + deletionTimeout :: Wear t f Word, + reminderTimeouts :: Wear t f [Word] + } + deriving (Generic, BareB) + +deriving instance FunctorB (PreventAdminlessGroupsConfigB Covered) + +deriving instance ApplicativeB (PreventAdminlessGroupsConfigB Covered) + +deriving instance TraversableB (PreventAdminlessGroupsConfigB Covered) + +type PreventAdminlessGroupsConfig = PreventAdminlessGroupsConfigB Bare Identity + +deriving instance Eq PreventAdminlessGroupsConfig + +deriving instance Show PreventAdminlessGroupsConfig + +deriving via (RenderableTypeName PreventAdminlessGroupsConfig) instance (RenderableSymbol PreventAdminlessGroupsConfig) + +deriving via (GenericUniform PreventAdminlessGroupsConfig) instance (Arbitrary PreventAdminlessGroupsConfig) + +deriving via (BarbieFeature PreventAdminlessGroupsConfigB) instance (ParseDbFeature PreventAdminlessGroupsConfig) + +deriving via (BarbieFeature PreventAdminlessGroupsConfigB) instance (ToSchema PreventAdminlessGroupsConfig) + +instance Default PreventAdminlessGroupsConfig where + def = + PreventAdminlessGroupsConfig + { promotionStrategy = PromotionStrategyAlphabetical, + deletionTimeout = 7, + reminderTimeouts = [2, 4, 6] + } + +instance (Typeable f, FieldF f) => ToSchema (PreventAdminlessGroupsConfigB Covered f) where + schema = + object $ + PreventAdminlessGroupsConfig + <$> promotionStrategy .= fieldF "promotionStrategy" schema + <*> deletionTimeout .= fieldF "deletionTimeout" schema + <*> reminderTimeouts .= fieldF "reminderTimeouts" (array schema) + +instance Default (LockableFeature PreventAdminlessGroupsConfig) where + def = defLockedFeature + +instance ToObjectSchema PreventAdminlessGroupsConfig where + objectSchema = field "config" schema + +instance IsFeatureConfig PreventAdminlessGroupsConfig where + type FeatureSymbol PreventAdminlessGroupsConfig = "preventAdminlessGroups" + featureSingleton = FeatureSingletonPreventAdminlessGroupsConfig + ---------------------------------------------------------------------- -- ExposeInvitationURLsToTeamAdminConfig @@ -2222,6 +2299,7 @@ type Features = LimitedEventFanoutConfig, DomainRegistrationConfig, ChannelsConfig, + PreventAdminlessGroupsConfig, CellsConfig, AllowedGlobalOperationsConfig, ConsumableNotificationsConfig, diff --git a/libs/wire-api/src/Wire/API/Team/FeatureFlags.hs b/libs/wire-api/src/Wire/API/Team/FeatureFlags.hs index 638da672409..275371eafa6 100644 --- a/libs/wire-api/src/Wire/API/Team/FeatureFlags.hs +++ b/libs/wire-api/src/Wire/API/Team/FeatureFlags.hs @@ -286,6 +286,13 @@ newtype instance FeatureDefaults ChannelsConfig deriving (FromJSON, ToJSON) via Defaults (LockableFeature ChannelsConfig) deriving (ParseFeatureDefaults) via OptionalField ChannelsConfig +newtype instance FeatureDefaults PreventAdminlessGroupsConfig + = PreventAdminlessGroupsDefaults (LockableFeature PreventAdminlessGroupsConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON, ToJSON) via Defaults (LockableFeature PreventAdminlessGroupsConfig) + deriving (ParseFeatureDefaults) via OptionalField PreventAdminlessGroupsConfig + newtype instance FeatureDefaults CellsInternalConfig = CellsInternalDefaults (LockableFeature CellsInternalConfig) deriving stock (Eq, Show) diff --git a/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs index 4a13a8947b1..4747dc7faea 100644 --- a/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs +++ b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs @@ -149,3 +149,5 @@ instance GetFeatureConfig StealthUsersConfig instance GetFeatureConfig MeetingsConfig instance GetFeatureConfig MeetingsPremiumConfig + +instance GetFeatureConfig PreventAdminlessGroupsConfig diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 221da66c5b2..32cc9ac9cb2 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -289,6 +289,7 @@ allFeaturesAPI = <@> featureAPI1Full <@> featureAPI1Full <@> featureAPI1Full + <@> featureAPI1Full <@> featureAPI1Get <@> featureAPI1Full <@> featureAPI1Full @@ -316,6 +317,7 @@ featureAPI = <@> mkNamedAPI @'("ilock", EnforceFileDownloadLocationConfig) (updateLockStatus @EnforceFileDownloadLocationConfig) <@> mkNamedAPI @'("ilock", DomainRegistrationConfig) (updateLockStatus @DomainRegistrationConfig) <@> mkNamedAPI @'("ilock", ChannelsConfig) (updateLockStatus @ChannelsConfig) + <@> mkNamedAPI @'("ilock", PreventAdminlessGroupsConfig) (updateLockStatus @PreventAdminlessGroupsConfig) <@> mkNamedAPI @'("ilock", CellsConfig) (updateLockStatus @CellsConfig) <@> mkNamedAPI @'("ilock", ConsumableNotificationsConfig) (updateLockStatus @ConsumableNotificationsConfig) <@> mkNamedAPI @'("ilock", ChatBubblesConfig) (updateLockStatus @ChatBubblesConfig) diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 6fea67c5905..6d95420f5d8 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -68,6 +68,7 @@ featureAPI = <@> deprecatedFeatureAPI <@> mkNamedAPI @'("get", DomainRegistrationConfig) getFeature <@> featureAPIGetPut + <@> featureAPIGetPut <@> mkNamedAPI @'("get", CellsConfig) getFeature <@> mkNamedAPI @"put-CellsConfig@v13" setFeature <@> mkNamedAPI @'("put", CellsConfig) setFeature diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 46709503d44..f04363a6966 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -419,6 +419,8 @@ instance SetFeatureConfig MLSConfig where instance SetFeatureConfig ChannelsConfig +instance SetFeatureConfig PreventAdminlessGroupsConfig + instance SetFeatureConfig ExposeInvitationURLsToTeamAdminConfig instance SetFeatureConfig OutlookCalIntegrationConfig diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 8c37c89071a..d53bbbfcd72 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -215,10 +215,13 @@ sitemap' = :<|> Named @"domain-registration-put" (mkFeatureStatusPutRoute @DomainRegistrationConfig) :<|> Named @"channels-get" (mkFeatureGetRoute @ChannelsConfig) :<|> Named @"channels-put" (mkFeaturePutRoute @ChannelsConfig) + :<|> Named @"prevent-adminless-groups-get" (mkFeatureGetRoute @PreventAdminlessGroupsConfig) + :<|> Named @"prevent-adminless-groups-put" (mkFeaturePutRoute @PreventAdminlessGroupsConfig) :<|> Named @"lock-unlock-route-outlook-cal-config" (mkFeatureLockUnlockRoute @OutlookCalIntegrationConfig) :<|> Named @"lock-unlock-route-enforce-file-download-location" (mkFeatureLockUnlockRoute @EnforceFileDownloadLocationConfig) :<|> Named @"domain-registration-lock" (mkFeatureLockUnlockRoute @DomainRegistrationConfig) :<|> Named @"channels-lock" (mkFeatureLockUnlockRoute @ChannelsConfig) + :<|> Named @"prevent-adminless-groups-lock" (mkFeatureLockUnlockRoute @PreventAdminlessGroupsConfig) :<|> Named @"lock-unlock-route-digital-signatures-config" (mkFeatureLockUnlockRoute @DigitalSignaturesConfig) :<|> Named @"lock-unlock-route-file-sharing-config" (mkFeatureLockUnlockRoute @FileSharingConfig) :<|> Named @"lock-unlock-route-conference-calling-config" (mkFeatureLockUnlockRoute @ConferenceCallingConfig) diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 21c89c56b04..668f6755360 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -462,6 +462,8 @@ type SternAPI = :<|> Named "domain-registration-put" (MkFeatureStatusPutRoute DomainRegistrationConfig) :<|> Named "channels-get" (MkFeatureGetRoute ChannelsConfig) :<|> Named "channels-put" (MkFeaturePutRoute ChannelsConfig) + :<|> Named "prevent-adminless-groups-get" (MkFeatureGetRoute PreventAdminlessGroupsConfig) + :<|> Named "prevent-adminless-groups-put" (MkFeaturePutRoute PreventAdminlessGroupsConfig) :<|> Named "lock-unlock-route-outlook-cal-config" (MkFeatureLockUnlockRoute OutlookCalIntegrationConfig) :<|> Named "lock-unlock-route-enforce-file-download-location" @@ -471,6 +473,7 @@ type SternAPI = ) :<|> Named "domain-registration-lock" (MkFeatureLockUnlockRoute DomainRegistrationConfig) :<|> Named "channels-lock" (MkFeatureLockUnlockRoute ChannelsConfig) + :<|> Named "prevent-adminless-groups-lock" (MkFeatureLockUnlockRoute PreventAdminlessGroupsConfig) :<|> Named "lock-unlock-route-digital-signatures-config" (MkFeatureLockUnlockRoute DigitalSignaturesConfig) :<|> Named "lock-unlock-route-file-sharing-config" (MkFeatureLockUnlockRoute FileSharingConfig) :<|> Named "lock-unlock-route-conference-calling-config" (MkFeatureLockUnlockRoute ConferenceCallingConfig) diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 6a29dce2e04..b1c6d1fc0d8 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -108,6 +108,8 @@ tests s = test s "/teams/:tid/features/cells" testCellsConfigRoutes, test s "/teams/:tid/features/channels" $ testLockedFeatureConfig @ChannelsConfig, test s "PUT /teams/:tid/features/channels{,'?lockOrUnlock'}" $ testLockStatus @ChannelsConfig, + test s "/teams/:tid/features/preventAdminlessGroups" $ testLockedFeatureConfig @PreventAdminlessGroupsConfig, + test s "PUT /teams/:tid/features/preventAdminlessGroups{,'?lockOrUnlock'}" $ testLockStatus @PreventAdminlessGroupsConfig, test s "PUT /teams/:tid/features/digitalSignatures{,'?lockOrUnlock'}" $ testLockStatus @DigitalSignaturesConfig, test s "PUT /teams/:tid/features/fileSharing{,'?lockOrUnlock'}" $ testLockStatus @FileSharingConfig, test s "PUT /teams/:tid/features/conference-calling{,'?lockOrUnlock'}" $ testLockStatus @ConferenceCallingConfig,