Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2-features/WPB-25314
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a team feature to configure adminless group prevention.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe mention that it's not doing anything yet?

1 change: 1 addition & 0 deletions integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions integration/test/Test/FeatureFlags/PreventAdminlessGroups.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2025 Wire Swiss GmbH <opensource@wire.com>
--
-- 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 <https://www.gnu.org/licenses/>.

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])
]
]
28 changes: 27 additions & 1 deletion integration/test/Test/FeatureFlags/Util.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ type IFeatureAPI =
:<|> IFeatureStatusLockStatusPut EnforceFileDownloadLocationConfig
:<|> IFeatureStatusLockStatusPut DomainRegistrationConfig
:<|> IFeatureStatusLockStatusPut ChannelsConfig
:<|> IFeatureStatusLockStatusPut PreventAdminlessGroupsConfig
:<|> IFeatureStatusLockStatusPut CellsConfig
:<|> IFeatureStatusLockStatusPut ConsumableNotificationsConfig
:<|> IFeatureStatusLockStatusPut ChatBubblesConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions libs/wire-api/src/Wire/API/Team/Feature.hs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ module Wire.API.Team.Feature
ChannelsConfig,
ChannelsConfigB (..),
ChannelPermissions (..),
PreventAdminlessGroupsConfig,
PreventAdminlessGroupsConfigB (..),
PreventAdminlessGroupsPromotionStrategy (..),
OutlookCalIntegrationConfig (..),
UseProxyOnMobile (..),
MlsE2EIdConfigB (..),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -2222,6 +2299,7 @@ type Features =
LimitedEventFanoutConfig,
DomainRegistrationConfig,
ChannelsConfig,
PreventAdminlessGroupsConfig,
CellsConfig,
AllowedGlobalOperationsConfig,
ConsumableNotificationsConfig,
Expand Down
7 changes: 7 additions & 0 deletions libs/wire-api/src/Wire/API/Team/FeatureFlags.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,5 @@ instance GetFeatureConfig StealthUsersConfig
instance GetFeatureConfig MeetingsConfig

instance GetFeatureConfig MeetingsPremiumConfig

instance GetFeatureConfig PreventAdminlessGroupsConfig
2 changes: 2 additions & 0 deletions services/galley/src/Galley/API/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ allFeaturesAPI =
<@> featureAPI1Full
<@> featureAPI1Full
<@> featureAPI1Full
<@> featureAPI1Full
<@> featureAPI1Get
<@> featureAPI1Full
<@> featureAPI1Full
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions services/galley/src/Galley/API/Public/Feature.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions services/galley/src/Galley/API/Teams/Features.hs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@ instance SetFeatureConfig MLSConfig where

instance SetFeatureConfig ChannelsConfig

instance SetFeatureConfig PreventAdminlessGroupsConfig

instance SetFeatureConfig ExposeInvitationURLsToTeamAdminConfig

instance SetFeatureConfig OutlookCalIntegrationConfig
Expand Down
3 changes: 3 additions & 0 deletions tools/stern/src/Stern/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions tools/stern/src/Stern/API/Routes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions tools/stern/test/integration/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down