diff --git a/changelog.d/2-features/pr-1976 b/changelog.d/2-features/pr-1976 new file mode 100644 index 0000000000..e67dd816a0 --- /dev/null +++ b/changelog.d/2-features/pr-1976 @@ -0,0 +1 @@ +If the guest links team feature is disabled guest links will be revoked. diff --git a/libs/wire-api/src/Wire/API/ErrorDescription.hs b/libs/wire-api/src/Wire/API/ErrorDescription.hs index f02a37cc0f..1cb53ce4a3 100644 --- a/libs/wire-api/src/Wire/API/ErrorDescription.hs +++ b/libs/wire-api/src/Wire/API/ErrorDescription.hs @@ -261,6 +261,8 @@ type HandleNotFound = ErrorDescription 404 "not-found" "Handle not found" type TooManyClients = ErrorDescription 403 "too-many-clients" "Too many clients" +type GuestLinksDisabled = ErrorDescription 409 "guest-links-disabled" "The guest link feature is disabled and all guest links have been revoked." + type MissingAuth = ErrorDescription 403 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 158fddcca3..30e402c059 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -195,6 +195,7 @@ data Api routes = Api :> CanThrow CodeNotFound :> CanThrow ConvNotFound :> CanThrow ConvAccessDenied + :> CanThrow GuestLinksDisabled :> ZLocalUser :> "conversations" :> "join" diff --git a/services/galley/src/Galley/API/Error.hs b/services/galley/src/Galley/API/Error.hs index a4569e788c..2e5a8a9afd 100644 --- a/services/galley/src/Galley/API/Error.hs +++ b/services/galley/src/Galley/API/Error.hs @@ -122,6 +122,7 @@ data ConversationError | ConvMemberNotFound | NoBindingTeamMembers | NoManagedTeamConv + | GuestLinksDisabled instance APIError ConversationError where toWai ConvAccessDenied = errorDescriptionTypeToWai @ConvAccessDenied @@ -130,6 +131,7 @@ instance APIError ConversationError where toWai ConvMemberNotFound = errorDescriptionTypeToWai @ConvMemberNotFound toWai NoBindingTeamMembers = noBindingTeamMembers toWai NoManagedTeamConv = noManagedTeamConv + toWai GuestLinksDisabled = guestLinksDisabled data TeamError = NoBindingTeam @@ -396,6 +398,9 @@ teamMemberNotFound = mkError status404 "no-team-member" "team member not found" noManagedTeamConv :: Error noManagedTeamConv = mkError status400 "no-managed-team-conv" "Managed team conversations have been deprecated." +guestLinksDisabled :: Error +guestLinksDisabled = mkError status409 "guest-links-disabled" "The guest link feature is disabled and all guest links have been revoked." + userBindingExists :: Error userBindingExists = mkError status403 "binding-exists" "User already bound to a different team." diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index b2c678d837..ace30d6eec 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -34,6 +34,7 @@ module Galley.API.Query where import qualified Cassandra as C +import Control.Lens import qualified Data.ByteString.Lazy as LBS import Data.Code import Data.CommaSeparatedList @@ -54,9 +55,12 @@ import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FederatorAccess as E import qualified Galley.Effects.ListItems as E import qualified Galley.Effects.MemberStore as E +import qualified Galley.Effects.TeamFeatureStore as TeamFeatures +import Galley.Options import Galley.Types import Galley.Types.Conversations.Members import Galley.Types.Conversations.Roles +import Galley.Types.Teams import Imports import Network.HTTP.Types import Network.Wai @@ -77,6 +81,7 @@ import qualified Wire.API.Federation.API.Galley as F import Wire.API.Federation.Error import qualified Wire.API.Provider.Bot as Public import qualified Wire.API.Routes.MultiTablePaging as Public +import Wire.API.Team.Feature as Public getBotConversationH :: Members '[ConversationStore, Error ConversationError, Input (Local ())] r => @@ -490,16 +495,17 @@ getConversationMeta cnv = do pure Nothing getConversationByReusableCode :: - Members - '[ BrigAccess, - CodeStore, - ConversationStore, - Error CodeError, - Error ConversationError, - Error NotATeamMember, - TeamStore - ] - r => + forall r. + ( Member BrigAccess r, + Member CodeStore r, + Member ConversationStore r, + Member (Error CodeError) r, + Member (Error ConversationError) r, + Member (Error NotATeamMember) r, + Member TeamStore r, + Member TeamFeatureStore r, + Member (Input Opts) r + ) => Local UserId -> Key -> Value -> @@ -507,7 +513,9 @@ getConversationByReusableCode :: getConversationByReusableCode lusr key value = do c <- verifyReusableCode (ConversationCode key value Nothing) conv <- ensureConversationAccess (tUnqualified lusr) (Data.codeConversation c) CodeAccess - pure $ coverView conv + getFeatureStatus conv >>= \case + TeamFeatureEnabled -> pure $ coverView conv + TeamFeatureDisabled -> throw GuestLinksDisabled where coverView :: Data.Conversation -> ConversationCoverView coverView conv = @@ -515,3 +523,13 @@ getConversationByReusableCode lusr key value = do { cnvCoverConvId = Data.convId conv, cnvCoverName = Data.convName conv } + + getDefaultFeatureStatus :: Sem r TeamFeatureStatusValue + getDefaultFeatureStatus = + input <&> view (optSettings . setFeatureFlags . flagConversationGuestLinks . unDefaults . to tfwoapsStatus) + + getFeatureStatus :: Data.Conversation -> Sem r TeamFeatureStatusValue + getFeatureStatus conv = do + defaultStatus <- getDefaultFeatureStatus + maybeFeatureStatus <- join <$> TeamFeatures.getFeatureStatusNoConfig @'TeamFeatureGuestLinks `traverse` Data.convTeam conv + pure $ maybe defaultStatus tfwoStatus maybeFeatureStatus diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 63ef91cd19..ff250f4d21 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -17,6 +17,7 @@ module Galley.API.Teams.Features ( getFeatureStatus, + getFeatureStatusNoConfig, setFeatureStatus, getFeatureConfig, getAllFeatureConfigs, diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 13d41e2b80..3d2706dd4e 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -33,6 +33,8 @@ import qualified API.Teams.Feature as TeamFeature import qualified API.Teams.LegalHold as Teams.LegalHold import qualified API.Teams.LegalHold.DisabledByDefault import API.Util +import qualified API.Util as Util +import API.Util.TeamFeature as TeamFeatures import Bilge hiding (timeout) import Bilge.Assert import Brig.Types @@ -93,6 +95,7 @@ import Wire.API.Federation.API.Galley import qualified Wire.API.Federation.API.Galley as F import qualified Wire.API.Message as Message import Wire.API.Routes.MultiTablePaging +import qualified Wire.API.Team.Feature as Public import Wire.API.User.Client import Wire.API.UserMap (UserMap (..)) @@ -218,6 +221,8 @@ tests s = test s "convert code to team-access conversation" postConvertTeamConv, test s "local and remote guests are removed when access changes" testAccessUpdateGuestRemoved, test s "cannot join private conversation" postJoinConvFail, + test s "revoke guest links for team conversation" testJoinTeamConvGuestLinksDisabled, + test s "revoke guest links for non-team conversation" testJoinNonTeamConvGuestLinksDisabled, test s "remove user with only local convs" removeUserNoFederation, test s "remove user with local and remote convs" removeUser, test s "iUpsertOne2OneConversation" testAllOne2OneConversationRequests, @@ -1236,6 +1241,60 @@ testJoinCodeConv = do getJoinCodeConv eve (conversationKey cCode) (conversationCode cCode) !!! do const 403 === statusCode +testJoinTeamConvGuestLinksDisabled :: TestM () +testJoinTeamConvGuestLinksDisabled = do + galley <- view tsGalley + let convName = "testConversation" + (owner, teamId, []) <- Util.createBindingTeamWithNMembers 0 + userNotInTeam <- randomUser + convId <- decodeConvId <$> postTeamConv teamId owner [] (Just convName) [CodeAccess] (Just ActivatedAccessRole) Nothing + cCode <- decodeConvCodeEvent <$> postConvCode owner convId + + -- works by default + getJoinCodeConv userNotInTeam (conversationKey cCode) (conversationCode cCode) !!! do + const (Right (ConversationCoverView convId (Just convName))) === responseJsonEither + const 200 === statusCode + + -- fails if disabled + let tfStatus = Public.TeamFeatureStatusNoConfig Public.TeamFeatureDisabled + TeamFeatures.putTeamFeatureFlagWithGalley @'Public.TeamFeatureGuestLinks galley owner teamId tfStatus !!! do + const 200 === statusCode + + getJoinCodeConv userNotInTeam (conversationKey cCode) (conversationCode cCode) !!! do + const 409 === statusCode + + -- after re-enabling, the old link is still valid + let tfStatus' = Public.TeamFeatureStatusNoConfig Public.TeamFeatureEnabled + TeamFeatures.putTeamFeatureFlagWithGalley @'Public.TeamFeatureGuestLinks galley owner teamId tfStatus' !!! do + const 200 === statusCode + + getJoinCodeConv userNotInTeam (conversationKey cCode) (conversationCode cCode) !!! do + const (Right (ConversationCoverView convId (Just convName))) === responseJsonEither + const 200 === statusCode + +testJoinNonTeamConvGuestLinksDisabled :: TestM () +testJoinNonTeamConvGuestLinksDisabled = do + galley <- view tsGalley + let convName = "testConversation" + (owner, teamId, []) <- Util.createBindingTeamWithNMembers 0 + userNotInTeam <- randomUser + convId <- decodeConvId <$> postConv owner [] (Just convName) [CodeAccess] (Just ActivatedAccessRole) Nothing + cCode <- decodeConvCodeEvent <$> postConvCode owner convId + + -- works by default + getJoinCodeConv userNotInTeam (conversationKey cCode) (conversationCode cCode) !!! do + const (Right (ConversationCoverView convId (Just convName))) === responseJsonEither + const 200 === statusCode + + -- for non-team conversations it still works if status is disabled for the team but not server wide + let tfStatus = Public.TeamFeatureStatusNoConfig Public.TeamFeatureDisabled + TeamFeatures.putTeamFeatureFlagWithGalley @'Public.TeamFeatureGuestLinks galley owner teamId tfStatus !!! do + const 200 === statusCode + + getJoinCodeConv userNotInTeam (conversationKey cCode) (conversationCode cCode) !!! do + const (Right (ConversationCoverView convId (Just convName))) === responseJsonEither + const 200 === statusCode + postJoinCodeConvOk :: TestM () postJoinCodeConvOk = do c <- view tsCannon diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index b1cb862d55..31af5c651a 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -1738,7 +1738,10 @@ randomTeamCreator :: HasCallStack => TestM UserId randomTeamCreator = qUnqualified <$> randomUser' True True True randomUser' :: HasCallStack => Bool -> Bool -> Bool -> TestM (Qualified UserId) -randomUser' isCreator hasPassword hasEmail = do +randomUser' isCreator hasPassword hasEmail = userQualifiedId . selfUser <$> randomUserProfile' isCreator hasPassword hasEmail + +randomUserProfile' :: HasCallStack => Bool -> Bool -> Bool -> TestM SelfProfile +randomUserProfile' isCreator hasPassword hasEmail = do b <- view tsBrig e <- liftIO randomEmail let p = @@ -1747,8 +1750,7 @@ randomUser' isCreator hasPassword hasEmail = do <> ["password" .= defPassword | hasPassword] <> ["email" .= fromEmail e | hasEmail] <> ["team" .= Team.BindingNewTeam (Team.newNewTeam (unsafeRange "teamName") (unsafeRange "defaultIcon")) | isCreator] - selfProfile <- responseJsonUnsafe <$> (post (b . path "/i/users" . json p) (post (b . path "/i/users" . json p) TestM UserId ephemeralUser = do