diff --git a/changelog.d/2-features/pr-2503 b/changelog.d/2-features/pr-2503 new file mode 100644 index 00000000000..d07e98ebaaa --- /dev/null +++ b/changelog.d/2-features/pr-2503 @@ -0,0 +1 @@ +Team feature API now includes endpoints to get and set the `searchVisibilityInbound` feature diff --git a/docs/legacy/reference/config-options.md b/docs/legacy/reference/config-options.md index 2ef6b244804..33d8eb04445 100644 --- a/docs/legacy/reference/config-options.md +++ b/docs/legacy/reference/config-options.md @@ -73,9 +73,9 @@ IMPORTANT: If you switch this back to `disabled-permanently` from that have created them while it was allowed. This may change in the future. -### Team Feature teamSearchVisibility +### Team searchVisibility -The feature flag `teamSearchVisibility` affects the outbound search of user +The team flag `searchVisibility` affects the outbound search of user searches. If it is set to `no-name-outside-team` for a team then all users of that team will no longer be able to find users that are not part of their team when searching. This also includes finding other users by by providing their @@ -96,6 +96,9 @@ pull-down-menu "body": "no-name-outside-team" ``` +The team feature flag `teamSearchVisibility` determines whether it is allowed to change the `searchVisibility` setting or not. +The default is `disabled-by-default`. Note that whenever this feature setting is disabled the `searchVisibility` will be reset to `standard`. + The default setting that applies to all teams on the instance can be defined at configuration ```yaml @@ -104,10 +107,6 @@ settings: teamSearchVisibility: disabled-by-default # or enabled-by-default ``` -where disabled is equivalent to `standard` and enabled is equivalent to `no-name-outside-team`. Individual teams may ovewrite the default setting. - -On wire cloud the default setting is `standard`. - ### TeamFeature searchVisibilityInbound The team feature flag `searchVisibilityInbound` affects if the team's users are diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 4165916ef47..ce1f686034e 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -45,7 +45,7 @@ module Galley.Types.Teams unDefaults, FeatureSSO (..), FeatureLegalHold (..), - FeatureTeamSearchVisibility (..), + FeatureTeamSearchVisibilityAvailability (..), notTeamMember, findTeamMember, isTeamMember, @@ -140,7 +140,7 @@ newtype TeamCreationTime = TeamCreationTime data FeatureFlags = FeatureFlags { _flagSSO :: !FeatureSSO, _flagLegalHold :: !FeatureLegalHold, - _flagTeamSearchVisibility :: !FeatureTeamSearchVisibility, + _flagTeamSearchVisibility :: !FeatureTeamSearchVisibilityAvailability, _flagAppLockDefaults :: !(Defaults (ImplicitLockStatus AppLockConfig)), _flagClassifiedDomains :: !(ImplicitLockStatus ClassifiedDomainsConfig), _flagFileSharing :: !(Defaults (WithStatus FileSharingConfig)), @@ -180,9 +180,9 @@ data FeatureLegalHold -- | Default value for all teams that have not enabled or disabled this feature explicitly. -- See also 'Wire.API.Team.SearchVisibility.TeamSearchVisibilityEnabled', -- 'Wire.API.Team.SearchVisibility.TeamSearchVisibility'. -data FeatureTeamSearchVisibility - = FeatureTeamSearchVisibilityEnabledByDefault - | FeatureTeamSearchVisibilityDisabledByDefault +data FeatureTeamSearchVisibilityAvailability + = FeatureTeamSearchVisibilityAvailableByDefault + | FeatureTeamSearchVisibilityUnavailableByDefault deriving (Eq, Ord, Show, Enum, Bounded, Generic) -- NOTE: This is used only in the config and thus YAML... camelcase @@ -259,14 +259,14 @@ instance ToJSON FeatureLegalHold where toJSON FeatureLegalHoldDisabledByDefault = String "disabled-by-default" toJSON FeatureLegalHoldWhitelistTeamsAndImplicitConsent = String "whitelist-teams-and-implicit-consent" -instance FromJSON FeatureTeamSearchVisibility where - parseJSON (String "enabled-by-default") = pure FeatureTeamSearchVisibilityEnabledByDefault - parseJSON (String "disabled-by-default") = pure FeatureTeamSearchVisibilityDisabledByDefault +instance FromJSON FeatureTeamSearchVisibilityAvailability where + parseJSON (String "enabled-by-default") = pure FeatureTeamSearchVisibilityAvailableByDefault + parseJSON (String "disabled-by-default") = pure FeatureTeamSearchVisibilityUnavailableByDefault parseJSON bad = fail $ "FeatureSearchVisibility: " <> cs (encode bad) -instance ToJSON FeatureTeamSearchVisibility where - toJSON FeatureTeamSearchVisibilityEnabledByDefault = String "enabled-by-default" - toJSON FeatureTeamSearchVisibilityDisabledByDefault = String "disabled-by-default" +instance ToJSON FeatureTeamSearchVisibilityAvailability where + toJSON FeatureTeamSearchVisibilityAvailableByDefault = String "enabled-by-default" + toJSON FeatureTeamSearchVisibilityUnavailableByDefault = String "disabled-by-default" makeLenses ''TeamCreationTime makeLenses ''FeatureFlags diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs index 9abb4cde7f8..9530bd0cdb7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -1120,6 +1120,8 @@ type FeatureAPI = :<|> FeatureStatusPut '() SndFactorPasswordChallengeConfig :<|> FeatureStatusGet MLSConfig :<|> FeatureStatusPut '() MLSConfig + :<|> FeatureStatusGet SearchVisibilityInboundConfig + :<|> FeatureStatusPut '() SearchVisibilityInboundConfig :<|> AllFeatureConfigsUserGet :<|> AllFeatureConfigsTeamGet :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" LegalholdConfig diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 84ad9d3e310..6e2dbdbaaff 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -817,7 +817,8 @@ data AllFeatureConfigs = AllFeatureConfigs afcSelfDeletingMessages :: WithStatus SelfDeletingMessagesConfig, afcGuestLink :: WithStatus GuestLinksConfig, afcSndFactorPasswordChallenge :: WithStatus SndFactorPasswordChallengeConfig, - afcMLS :: WithStatus MLSConfig + afcMLS :: WithStatus MLSConfig, + afcSearchVisibilityInboundConfig :: WithStatus SearchVisibilityInboundConfig } deriving stock (Eq, Show) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AllFeatureConfigs) @@ -839,6 +840,7 @@ instance ToSchema AllFeatureConfigs where <*> afcGuestLink .= featureField <*> afcSndFactorPasswordChallenge .= featureField <*> afcMLS .= featureField + <*> afcSearchVisibilityInboundConfig .= featureField where featureField :: forall cfg. @@ -862,5 +864,6 @@ instance Arbitrary AllFeatureConfigs where <*> arbitrary <*> arbitrary <*> arbitrary + <*> arbitrary makeLenses ''ImplicitLockStatus diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 9b00f035fe2..a8de858a051 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -171,6 +171,9 @@ type IFeatureAPI = -- MLSConfig :<|> IFeatureStatusGet MLSConfig :<|> IFeatureStatusPut '() MLSConfig + -- SearchVisibilityInboundConfig + :<|> IFeatureStatusGet SearchVisibilityInboundConfig + :<|> IFeatureStatusPut '() SearchVisibilityInboundConfig -- all feature configs :<|> Named "feature-configs-internal" @@ -337,6 +340,9 @@ type ITeamsAPIBase = :<|> Named "set-search-visibility-internal" ( CanThrow 'TeamSearchVisibilityNotEnabled + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamNotFound :> ReqBody '[Servant.JSON] TeamSearchVisibilityView :> MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 204 "OK") ) @@ -434,7 +440,7 @@ iTeamsAPI = mkAPI $ \tid -> hoistAPIHandler id (base tid) <@> mkNamedAPI @"user-is-team-owner" (Teams.userIsTeamOwner tid) <@> hoistAPISegment ( mkNamedAPI @"get-search-visibility-internal" (Teams.getSearchVisibilityInternal tid) - <@> mkNamedAPI @"set-search-visibility-internal" (Teams.setSearchVisibilityInternal @Cassandra tid) + <@> mkNamedAPI @"set-search-visibility-internal" (Teams.setSearchVisibilityInternal @Cassandra (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig) tid) ) featureAPI :: API IFeatureAPI GalleyEffects @@ -471,6 +477,8 @@ featureAPI = <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) + <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) <@> mkNamedAPI (maybe (getAllFeatureConfigsForServer @Cassandra) (getAllFeatureConfigsForUser @Cassandra)) internalSitemap :: Routes a (Sem GalleyEffects) () diff --git a/services/galley/src/Galley/API/Public/Servant.hs b/services/galley/src/Galley/API/Public/Servant.hs index 9f821a80b29..b35276edf9f 100644 --- a/services/galley/src/Galley/API/Public/Servant.hs +++ b/services/galley/src/Galley/API/Public/Servant.hs @@ -119,7 +119,7 @@ servantSitemap = <@> mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra Nothing . DoAuth) <@> mkNamedAPI @"get-search-visibility" getSearchVisibility - <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility @Cassandra) + <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility @Cassandra (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig)) <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) @@ -138,6 +138,8 @@ servantSitemap = <@> mkNamedAPI @'("put", SndFactorPasswordChallengeConfig) (setFeatureStatus @Cassandra Nothing . DoAuth) <@> mkNamedAPI @'("get", MLSConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("put", MLSConfig) (setFeatureStatus @Cassandra Nothing . DoAuth) + <@> mkNamedAPI @'("get", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", SearchVisibilityInboundConfig) (setFeatureStatus @Cassandra Nothing . DoAuth) <@> mkNamedAPI @"get-all-feature-configs-for-user" (getAllFeatureConfigsForUser @Cassandra) <@> mkNamedAPI @"get-all-feature-configs-for-team" (getAllFeatureConfigsForTeam @Cassandra) <@> mkNamedAPI @'("get-config", LegalholdConfig) (getFeatureStatusForUser @Cassandra) diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 04345a60268..b9ab3f07a48 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -103,7 +103,6 @@ import qualified Galley.Effects.Queue as E import qualified Galley.Effects.SearchVisibilityStore as SearchVisibilityData import qualified Galley.Effects.SparAccess as Spar import Galley.Effects.TeamFeatureStore (FeaturePersistentConstraint) -import qualified Galley.Effects.TeamFeatureStore as TeamFeatures import qualified Galley.Effects.TeamMemberStore as E import qualified Galley.Effects.TeamStore as E import qualified Galley.Intra.Journal as Journal @@ -141,7 +140,6 @@ import Wire.API.Team.Conversation import qualified Wire.API.Team.Conversation as Public import Wire.API.Team.Export (TeamExportUser (..)) import Wire.API.Team.Feature -import qualified Wire.API.Team.Feature as Public import Wire.API.Team.Member (HardTruncationLimit, ListType (ListComplete, ListTruncated), NewTeamMember, TeamMember, TeamMemberList, TeamMemberListOptPerms, TeamMemberOptPerms, hardTruncationLimit, invitation, nPermissions, nUserId, newTeamMemberList, ntmNewTeamMember, permissions, setOptionalPerms, setOptionalPermsMany, teamMemberListType, teamMembers, tmdAuthPassword, userId) import qualified Wire.API.Team.Member as Public import Wire.API.Team.Permission (Perm (..), Permissions (..), SPerm (..), copy, fullPermissions, self) @@ -1101,14 +1099,15 @@ setSearchVisibility :: r, FeaturePersistentConstraint db SearchVisibilityAvailableConfig ) => + (TeamId -> Sem r Bool) -> Local UserId -> TeamId -> Public.TeamSearchVisibilityView -> Sem r () -setSearchVisibility luid tid req = do +setSearchVisibility availableForTeam luid tid req = do zusrMembership <- E.getTeamMember tid (tUnqualified luid) void $ permissionCheck ChangeTeamSearchVisibility zusrMembership - setSearchVisibilityInternal @db tid req + setSearchVisibilityInternal @db availableForTeam tid req -- Internal ----------------------------------------------------------------- @@ -1380,22 +1379,6 @@ canUserJoinTeam tid = do (TeamSize sizeBeforeJoin) <- E.getSize tid ensureNotTooLargeForLegalHold @db tid (fromIntegral sizeBeforeJoin + 1) -getTeamSearchVisibilityAvailableInternal :: - forall db r. - (Members '[Input Opts, TeamFeatureStore db] r, FeaturePersistentConstraint db SearchVisibilityAvailableConfig) => - TeamId -> - Sem r (WithStatus Public.SearchVisibilityAvailableConfig) -getTeamSearchVisibilityAvailableInternal tid = do - defConfig <- do - featureTeamSearchVisibility <- view (optSettings . setFeatureFlags . flagTeamSearchVisibility) <$> input - let status = case featureTeamSearchVisibility of - FeatureTeamSearchVisibilityEnabledByDefault -> Public.FeatureStatusEnabled - FeatureTeamSearchVisibilityDisabledByDefault -> Public.FeatureStatusDisabled - pure $ (defFeatureStatus @Public.SearchVisibilityAvailableConfig) {wsStatus = status} - mbConfig <- TeamFeatures.getFeatureConfig @db (Proxy @Public.SearchVisibilityAvailableConfig) tid - mbLockStatus <- TeamFeatures.getFeatureLockStatus @db (Proxy @Public.SearchVisibilityAvailableConfig) tid - pure $ computeFeatureConfigForTeamUser mbConfig mbLockStatus defConfig - -- | Modify and get visibility type for a team (internal, no user permission checks) getSearchVisibilityInternal :: Member SearchVisibilityStore r => @@ -1416,12 +1399,12 @@ setSearchVisibilityInternal :: r, FeaturePersistentConstraint db SearchVisibilityAvailableConfig ) => + (TeamId -> Sem r Bool) -> TeamId -> TeamSearchVisibilityView -> Sem r () -setSearchVisibilityInternal tid (TeamSearchVisibilityView searchVisibility) = do - fs <- getTeamSearchVisibilityAvailableInternal @db tid - unless (Public.wsStatus fs == Public.FeatureStatusEnabled) $ +setSearchVisibilityInternal availableForTeam tid (TeamSearchVisibilityView searchVisibility) = do + unlessM (availableForTeam tid) $ throwS @'TeamSearchVisibilityNotEnabled SearchVisibilityData.setSearchVisibility tid searchVisibility diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 1494fb609dd..f220f103a08 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -30,6 +30,7 @@ module Galley.API.Teams.Features SetFeatureConfig, guardSecondFactorDisabled, DoAuth (..), + featureEnabledForTeam, ) where @@ -174,7 +175,8 @@ type FeaturePersistentAllFeatures db = FeaturePersistentConstraint db SelfDeletingMessagesConfig, FeaturePersistentConstraint db GuestLinksConfig, FeaturePersistentConstraint db SndFactorPasswordChallengeConfig, - FeaturePersistentConstraint db MLSConfig + FeaturePersistentConstraint db MLSConfig, + FeaturePersistentConstraint db SearchVisibilityInboundConfig ) getFeatureStatus :: @@ -377,6 +379,7 @@ getAllFeatureConfigsForServer = <*> getConfigForServer @db @GuestLinksConfig <*> getConfigForServer @db @SndFactorPasswordChallengeConfig <*> getConfigForServer @db @MLSConfig + <*> getConfigForServer @db @SearchVisibilityInboundConfig getAllFeatureConfigsUser :: forall db r. @@ -409,6 +412,7 @@ getAllFeatureConfigsUser uid = <*> getConfigForUser @db @GuestLinksConfig uid <*> getConfigForUser @db @SndFactorPasswordChallengeConfig uid <*> getConfigForUser @db @MLSConfig uid + <*> getConfigForUser @db @SearchVisibilityInboundConfig uid getAllFeatureConfigsTeam :: forall db r. @@ -440,6 +444,7 @@ getAllFeatureConfigsTeam tid = <*> getConfigForTeam @db @GuestLinksConfig tid <*> getConfigForTeam @db @SndFactorPasswordChallengeConfig tid <*> getConfigForTeam @db @MLSConfig tid + <*> getConfigForTeam @db @SearchVisibilityInboundConfig tid -- | Note: this is an internal function which doesn't cover all features, e.g. LegalholdConfig genericGetConfigForTeam :: @@ -580,8 +585,8 @@ instance GetFeatureConfig db SearchVisibilityAvailableConfig where getConfigForServer = do status <- inputs (view (optSettings . setFeatureFlags . flagTeamSearchVisibility)) <&> \case - FeatureTeamSearchVisibilityEnabledByDefault -> FeatureStatusEnabled - FeatureTeamSearchVisibilityDisabledByDefault -> FeatureStatusDisabled + FeatureTeamSearchVisibilityAvailableByDefault -> FeatureStatusEnabled + FeatureTeamSearchVisibilityUnavailableByDefault -> FeatureStatusDisabled pure $ defFeatureStatus {wsStatus = status} instance SetFeatureConfig db SearchVisibilityAvailableConfig where @@ -814,3 +819,19 @@ guardSecondFactorDisabled uid cid action = do case wsStatus tf of FeatureStatusDisabled -> action FeatureStatusEnabled -> throwS @'AccessDenied + +featureEnabledForTeam :: + forall db cfg r. + ( GetFeatureConfig db cfg, + GetConfigForTeamConstraints db cfg r, + Members + '[ ErrorS OperationDenied, + ErrorS 'NotATeamMember, + ErrorS 'TeamNotFound, + TeamStore + ] + r + ) => + TeamId -> + Sem r Bool +featureEnabledForTeam tid = (==) FeatureStatusEnabled . wsStatus <$> getFeatureStatus @db @cfg DontDoAuth tid diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index e4fdc27ccab..18e91862ac8 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -477,12 +477,12 @@ testEnableTeamSearchVisibilityPerTeam = do const 200 === statusCode const (Just (TeamSearchVisibilityView vis)) === responseJsonUnsafe - Util.withCustomSearchFeature FeatureTeamSearchVisibilityEnabledByDefault $ do + Util.withCustomSearchFeature FeatureTeamSearchVisibilityAvailableByDefault $ do g <- view tsGalley check "Teams should start with Custom Search Visibility enabled" Public.FeatureStatusEnabled putSearchVisibility g owner tid SearchVisibilityNoNameOutsideTeam !!! const 204 === statusCode putSearchVisibility g owner tid SearchVisibilityStandard !!! const 204 === statusCode - Util.withCustomSearchFeature FeatureTeamSearchVisibilityDisabledByDefault $ do + Util.withCustomSearchFeature FeatureTeamSearchVisibilityUnavailableByDefault $ do check "Teams should start with Custom Search Visibility disabled" Public.FeatureStatusDisabled putSearchVisibilityCheckNotAllowed diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index b88e9394133..eb1262b3e87 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -26,7 +26,7 @@ import Bilge.Assert import Cassandra as Cql import Control.Lens (over, to, view) import Control.Monad.Catch (MonadCatch) -import Data.Aeson (FromJSON, ToJSON, object, (.=)) +import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as Aeson import qualified Data.Aeson.Key as AesonKey import qualified Data.Aeson.KeyMap as KeyMap @@ -42,7 +42,7 @@ import Galley.Options (optSettings, setFeatureFlags) import Galley.Types.Teams import Imports import Network.Wai.Utilities (label) -import Test.Hspec (expectationFailure, shouldBe) +import Test.Hspec (expectationFailure) import Test.Tasty import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit (assertFailure, (@?=)) @@ -93,7 +93,8 @@ tests s = test s "reduce" $ testSimpleFlagTTLOverride @Public.ConferenceCallingConfig Public.FeatureStatusEnabled (FeatureTTLSeconds 5) (FeatureTTLSeconds 1), test s "Unlimited to unlimited" $ testSimpleFlagTTLOverride @Public.ConferenceCallingConfig Public.FeatureStatusEnabled FeatureTTLUnlimited FeatureTTLUnlimited ], - test s "MLS feature config" testMLS + test s "MLS feature config" testMLS, + test s "SearchVisibilityInbound" $ testSimpleFlag @Public.SearchVisibilityInboundConfig Public.FeatureStatusDisabled ] testSSO :: TestM () @@ -108,7 +109,9 @@ testSSO = do let getSSO :: HasCallStack => Public.FeatureStatus -> TestM () getSSO = assertFlagNoConfig @Public.SSOConfig $ Util.getTeamFeatureFlag @Public.SSOConfig member tid getSSOFeatureConfig :: HasCallStack => Public.FeatureStatus -> TestM () - getSSOFeatureConfig = assertFlagNoConfig @Public.SSOConfig $ Util.getFeatureConfig @Public.SSOConfig member + getSSOFeatureConfig expectedStatus = do + actual <- Util.getFeatureConfig @Public.SSOConfig member + liftIO $ Public.wsStatus actual @?= expectedStatus getSSOInternal :: HasCallStack => Public.FeatureStatus -> TestM () getSSOInternal = assertFlagNoConfig @Public.SSOConfig $ Util.getTeamFeatureFlagInternal @Public.SSOConfig tid setSSOInternal :: HasCallStack => Public.FeatureStatus -> TestM () @@ -149,7 +152,9 @@ testLegalHold = do getLegalHold = assertFlagNoConfig @Public.LegalholdConfig $ Util.getTeamFeatureFlag @Public.LegalholdConfig member tid getLegalHoldInternal :: HasCallStack => Public.FeatureStatus -> TestM () getLegalHoldInternal = assertFlagNoConfig @Public.LegalholdConfig $ Util.getTeamFeatureFlagInternal @Public.LegalholdConfig tid - getLegalHoldFeatureConfig = assertFlagNoConfig @Public.LegalholdConfig $ Util.getFeatureConfig @Public.LegalholdConfig member + getLegalHoldFeatureConfig expectedStatus = do + actual <- Util.getFeatureConfig @Public.LegalholdConfig member + liftIO $ Public.wsStatus actual @?= expectedStatus setLegalHoldInternal :: HasCallStack => Public.FeatureStatus -> TestM () setLegalHoldInternal = void . Util.putTeamFeatureFlagInternal @Public.LegalholdConfig expect2xx tid . (`Public.WithStatusNoLock` Public.LegalholdConfig) @@ -206,10 +211,8 @@ testSearchVisibility = do let getTeamSearchVisibilityFeatureConfig :: UserId -> Public.FeatureStatus -> TestM () getTeamSearchVisibilityFeatureConfig uid expected = do - g <- view tsGalley - Util.getFeatureConfigWithGalley @Public.SearchVisibilityAvailableConfig g uid !!! do - statusCode === const 200 - responseJsonEither === const (Right (Public.WithStatusNoLock expected Public.SearchVisibilityAvailableConfig)) + actual <- Util.getFeatureConfig @Public.SearchVisibilityAvailableConfig uid + liftIO $ Public.wsStatus actual @?= expected let setTeamSearchVisibilityInternal :: TeamId -> Public.FeatureStatus -> TestM () setTeamSearchVisibilityInternal teamid val = do @@ -223,7 +226,7 @@ testSearchVisibility = do Util.connectUsers owner (list1 team2member []) Util.addTeamMember owner tid2 team2member (rolePermissions RoleMember) Nothing - Util.withCustomSearchFeature FeatureTeamSearchVisibilityDisabledByDefault $ do + Util.withCustomSearchFeature FeatureTeamSearchVisibilityUnavailableByDefault $ do getTeamSearchVisibility tid2 Public.FeatureStatusDisabled getTeamSearchVisibilityInternal tid2 Public.FeatureStatusDisabled getTeamSearchVisibilityFeatureConfig team2member Public.FeatureStatusDisabled @@ -243,7 +246,7 @@ testSearchVisibility = do Util.connectUsers owner (list1 team3member []) Util.addTeamMember owner tid3 team3member (rolePermissions RoleMember) Nothing - Util.withCustomSearchFeature FeatureTeamSearchVisibilityEnabledByDefault $ do + Util.withCustomSearchFeature FeatureTeamSearchVisibilityAvailableByDefault $ do getTeamSearchVisibility tid3 Public.FeatureStatusEnabled getTeamSearchVisibilityInternal tid3 Public.FeatureStatusEnabled getTeamSearchVisibilityFeatureConfig team3member Public.FeatureStatusEnabled @@ -288,9 +291,10 @@ testClassifiedDomainsEnabled = do UserId -> Public.WithStatusNoLock Public.ClassifiedDomainsConfig -> m () - getClassifiedDomainsFeatureConfig uid = do - assertFlagWithConfig @Public.ClassifiedDomainsConfig $ - Util.getFeatureConfig @Public.ClassifiedDomainsConfig uid + getClassifiedDomainsFeatureConfig uid expected' = do + result <- Util.getFeatureConfig @Public.ClassifiedDomainsConfig uid + liftIO $ Public.wsStatus result @?= Public.wssStatus expected' + liftIO $ Public.wsConfig result @?= Public.wssConfig expected' getClassifiedDomains member tid expected getClassifiedDomainsInternal tid expected @@ -307,9 +311,10 @@ testClassifiedDomainsDisabled = do UserId -> Public.WithStatusNoLock Public.ClassifiedDomainsConfig -> m () - getClassifiedDomainsFeatureConfig uid = do - assertFlagWithConfig @Public.ClassifiedDomainsConfig $ - Util.getFeatureConfig @Public.ClassifiedDomainsConfig uid + getClassifiedDomainsFeatureConfig uid expected' = do + result <- Util.getFeatureConfig @Public.ClassifiedDomainsConfig uid + liftIO $ Public.wsStatus result @?= Public.wssStatus expected' + liftIO $ Public.wsConfig result @?= Public.wssConfig expected' let classifiedDomainsDisabled = \opts -> opts @@ -364,8 +369,9 @@ testSimpleFlagTTLOverride defaultValue ttl ttlAfter = do flip (assertFlagNoConfig @cfg) expected $ Util.getTeamFeatureFlag @cfg member tid getFeatureConfig :: HasCallStack => Public.FeatureStatus -> TestM () - getFeatureConfig expected = - flip (assertFlagNoConfig @cfg) expected $ Util.getFeatureConfig @cfg member + getFeatureConfig expected = do + actual <- Util.getFeatureConfig @cfg member + liftIO $ Public.wsStatus actual @?= expected getFlagInternal :: HasCallStack => Public.FeatureStatus -> TestM () getFlagInternal expected = @@ -383,7 +389,7 @@ testSimpleFlagTTLOverride defaultValue ttl ttlAfter = do cassState <- view tsCass liftIO $ do storedTTL <- maybe Nothing runIdentity <$> Cql.runClient cassState (Cql.query1 select $ params LocalQuorum (Identity tid)) - storedTTL `shouldBe` Nothing + storedTTL @?= Nothing half = 500000 seconds = 1000000 @@ -476,8 +482,9 @@ testSimpleFlagTTL defaultValue ttl = do flip (assertFlagNoConfig @cfg) expected $ Util.getTeamFeatureFlag @cfg member tid getFeatureConfig :: HasCallStack => Public.FeatureStatus -> TestM () - getFeatureConfig expected = - flip (assertFlagNoConfig @cfg) expected $ Util.getFeatureConfig @cfg member + getFeatureConfig expected = do + actual <- Util.getFeatureConfig @cfg member + liftIO $ Public.wsStatus actual @?= expected getFlagInternal :: HasCallStack => Public.FeatureStatus -> TestM () getFlagInternal expected = @@ -523,7 +530,7 @@ testSimpleFlagTTL defaultValue ttl = do cassState <- view tsCass liftIO $ do storedTTL <- Cql.runClient cassState $ Cql.query1 select $ params LocalQuorum (Identity tid) - runIdentity <$> storedTTL `shouldBe` Just Nothing + runIdentity <$> storedTTL @?= Just Nothing -- Clean up setFlagInternal defaultValue FeatureTTLUnlimited @@ -561,8 +568,9 @@ testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do getFeatureConfig :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () getFeatureConfig expectedStatus expectedLockStatus = do - let flag = Util.getFeatureConfig @cfg member - assertFlagNoConfigWithLockStatus @cfg flag expectedStatus expectedLockStatus + actual <- Util.getFeatureConfig @cfg member + liftIO $ Public.wsStatus actual @?= expectedStatus + liftIO $ Public.wsLockStatus actual @?= expectedLockStatus getFlagInternal :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () getFlagInternal expectedStatus expectedLockStatus = do @@ -653,8 +661,9 @@ testSelfDeletingMessages = do (Public.SelfDeletingMessagesConfig tout) personalUser <- Util.randomUser - Util.getFeatureConfig @Public.SelfDeletingMessagesConfig personalUser - !!! responseJsonEither === const (Right $ settingWithLockStatus FeatureStatusEnabled 0 defLockStatus) + do + result <- Util.getFeatureConfig @Public.SelfDeletingMessagesConfig personalUser + liftIO $ result @?= settingWithLockStatus FeatureStatusEnabled 0 defLockStatus -- team users galley <- view tsGalley @@ -675,10 +684,11 @@ testSelfDeletingMessages = do let expected = settingWithLockStatus stat tout lockStatus forM_ [ Util.getTeamFeatureFlagInternal @Public.SelfDeletingMessagesConfig tid, - Util.getTeamFeatureFlagWithGalley @Public.SelfDeletingMessagesConfig galley owner tid, - Util.getFeatureConfig @Public.SelfDeletingMessagesConfig owner + Util.getTeamFeatureFlagWithGalley @Public.SelfDeletingMessagesConfig galley owner tid ] (!!! responseJsonEither === const (Right expected)) + result <- Util.getFeatureConfig @Public.SelfDeletingMessagesConfig owner + liftIO $ result @?= expected checkSetLockStatus :: HasCallStack => Public.LockStatus -> TestM () checkSetLockStatus status = @@ -814,30 +824,22 @@ testAllFeatures = do responseJsonMaybe === const (Just (expected FeatureStatusEnabled defLockStatus {- determined by 'getAfcConferenceCallingDefNew' in brig -})) where expected confCalling lockStateSelfDeleting = - object - [ toS @Public.LegalholdConfig .= Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.LegalholdConfig, - toS @Public.SSOConfig .= Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SSOConfig, - toS @Public.SearchVisibilityAvailableConfig .= Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityAvailableConfig, - toS @Public.ValidateSAMLEmailsConfig .= Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.ValidateSAMLEmailsConfig, - toS @Public.DigitalSignaturesConfig .= Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.DigitalSignaturesConfig, - toS @Public.AppLockConfig - .= Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.AppLockConfig (Public.EnforceAppLock False) (60 :: Int32)), - toS @Public.FileSharingConfig .= Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.FileSharingConfig, - toS @Public.ClassifiedDomainsConfig - .= Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.ClassifiedDomainsConfig [Domain "example.com"]), - toS @Public.ConferenceCallingConfig - .= Public.WithStatus confCalling Public.LockStatusUnlocked Public.ConferenceCallingConfig, - toS @Public.SelfDeletingMessagesConfig - .= Public.WithStatus FeatureStatusEnabled lockStateSelfDeleting (Public.SelfDeletingMessagesConfig 0), - toS @Public.GuestLinksConfig .= Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.GuestLinksConfig, - toS @Public.ValidateSAMLEmailsConfig .= Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.GuestLinksConfig, - toS @Public.GuestLinksConfig .= Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.GuestLinksConfig, - toS @Public.SndFactorPasswordChallengeConfig .= Public.WithStatus FeatureStatusDisabled Public.LockStatusLocked Public.SndFactorPasswordChallengeConfig, - toS @Public.MLSConfig .= Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked (Public.MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) - ] - - toS :: forall cfg. (Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg)) => Aeson.Key - toS = AesonKey.fromText (Public.featureName @cfg) + Public.AllFeatureConfigs + { Public.afcLegalholdStatus = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.LegalholdConfig, + Public.afcSSOStatus = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SSOConfig, + Public.afcTeamSearchVisibilityAvailable = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityAvailableConfig, + Public.afcValidateSAMLEmails = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.ValidateSAMLEmailsConfig, + Public.afcDigitalSignatures = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.DigitalSignaturesConfig, + Public.afcAppLock = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.AppLockConfig (Public.EnforceAppLock False) (60 :: Int32)), + Public.afcFileSharing = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.FileSharingConfig, + Public.afcClassifiedDomains = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.ClassifiedDomainsConfig [Domain "example.com"]), + Public.afcConferenceCalling = Public.WithStatus confCalling Public.LockStatusUnlocked Public.ConferenceCallingConfig, + Public.afcSelfDeletingMessages = Public.WithStatus FeatureStatusEnabled lockStateSelfDeleting (Public.SelfDeletingMessagesConfig 0), + Public.afcGuestLink = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.GuestLinksConfig, + Public.afcSndFactorPasswordChallenge = Public.WithStatus FeatureStatusDisabled Public.LockStatusLocked Public.SndFactorPasswordChallengeConfig, + Public.afcMLS = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked (Public.MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519), + Public.afcSearchVisibilityInboundConfig = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityInboundConfig + } testFeatureConfigConsistency :: TestM () testFeatureConfigConsistency = do @@ -935,8 +937,10 @@ testMLS = do flip assertFlagWithConfig expected $ Util.getTeamFeatureFlagInternal @Public.MLSConfig tid getForUser :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () - getForUser expected = - flip assertFlagWithConfig expected $ Util.getFeatureConfig @MLSConfig member + getForUser expected = do + result <- Util.getFeatureConfig @MLSConfig member + liftIO $ Public.wsStatus result @?= Public.wssStatus expected + liftIO $ Public.wsConfig result @?= Public.wssConfig expected getViaEndpoints :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () getViaEndpoints expected = do diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index 4332dc97672..db5cd46b2b9 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -20,8 +20,10 @@ module API.Util.TeamFeature where import API.Util (HasGalley (viewGalley), zUser) import qualified API.Util as Util import Bilge -import Control.Lens (view, (.~)) -import Data.Aeson (ToJSON) +import Control.Lens (view, (.~), (^?)) +import Control.Monad.Catch (MonadThrow) +import Data.Aeson (FromJSON, Result (Success), ToJSON, Value, fromJSON) +import Data.Aeson.Lens import Data.ByteString.Conversion (toByteString') import Data.Id (ConvId, TeamId, UserId) import Data.Schema @@ -33,7 +35,7 @@ import TestSetup import Wire.API.Team.Feature (IsFeatureConfig) import qualified Wire.API.Team.Feature as Public -withCustomSearchFeature :: FeatureTeamSearchVisibility -> TestM () -> TestM () +withCustomSearchFeature :: FeatureTeamSearchVisibilityAvailability -> TestM () -> TestM () withCustomSearchFeature flag action = do Util.withSettingsOverrides (\opts -> opts & optSettings . setFeatureFlags . flagTeamSearchVisibility .~ flag) action @@ -126,17 +128,16 @@ getTeamFeatureFlagWithGalley galley uid tid = do . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] . zUser uid -getFeatureConfig :: forall cfg m. (HasCallStack, HasGalley m, MonadIO m, MonadHttp m, IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg)) => UserId -> m ResponseLBS +getFeatureConfig :: forall cfg m. (HasCallStack, MonadThrow m, HasGalley m, MonadIO m, MonadHttp m, IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg), FromJSON (Public.WithStatus cfg)) => UserId -> m (Public.WithStatus cfg) getFeatureConfig uid = do - g <- viewGalley - getFeatureConfigWithGalley @cfg g uid - -getFeatureConfigWithGalley :: forall cfg m. (MonadIO m, MonadHttp m, HasCallStack, IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg)) => (Request -> Request) -> UserId -> m ResponseLBS -getFeatureConfigWithGalley galley uid = do - get $ - galley - . paths ["feature-configs", toByteString' (Public.featureNameBS @cfg)] - . zUser uid + galley <- viewGalley + response :: Value <- responseJsonError =<< getAllFeatureConfigsWithGalley galley uid + let status = response ^? key (Public.featureName @cfg) + maybe (error "getting all features failed") pure (status >>= fromResult . fromJSON) + where + fromResult :: Result a -> Maybe a + fromResult (Success b) = Just b + fromResult _ = Nothing getAllFeatureConfigs :: HasCallStack => UserId -> TestM ResponseLBS getAllFeatureConfigs uid = do