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/pr-1976
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If the guest links team feature is disabled guest links will be revoked.
2 changes: 2 additions & 0 deletions libs/wire-api/src/Wire/API/ErrorDescription.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions libs/wire-api/src/Wire/API/Routes/Public/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ data Api routes = Api
:> CanThrow CodeNotFound
:> CanThrow ConvNotFound
:> CanThrow ConvAccessDenied
:> CanThrow GuestLinksDisabled
:> ZLocalUser
:> "conversations"
:> "join"
Expand Down
5 changes: 5 additions & 0 deletions services/galley/src/Galley/API/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ data ConversationError
| ConvMemberNotFound
| NoBindingTeamMembers
| NoManagedTeamConv
| GuestLinksDisabled

instance APIError ConversationError where
toWai ConvAccessDenied = errorDescriptionTypeToWai @ConvAccessDenied
Expand All @@ -130,6 +131,7 @@ instance APIError ConversationError where
toWai ConvMemberNotFound = errorDescriptionTypeToWai @ConvMemberNotFound
toWai NoBindingTeamMembers = noBindingTeamMembers
toWai NoManagedTeamConv = noManagedTeamConv
toWai GuestLinksDisabled = guestLinksDisabled

data TeamError
= NoBindingTeam
Expand Down Expand Up @@ -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."

Expand Down
40 changes: 29 additions & 11 deletions services/galley/src/Galley/API/Query.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 =>
Expand Down Expand Up @@ -490,28 +495,41 @@ 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 ->
Sem r ConversationCoverView
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 =
ConversationCoverView
{ 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
1 change: 1 addition & 0 deletions services/galley/src/Galley/API/Teams/Features.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

module Galley.API.Teams.Features
( getFeatureStatus,
getFeatureStatusNoConfig,
setFeatureStatus,
getFeatureConfig,
getAllFeatureConfigs,
Expand Down
59 changes: 59 additions & 0 deletions services/galley/test/integration/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (..))

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions services/galley/test/integration/API/Util.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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) <!! const 201 === statusCode)
pure . userQualifiedId . selfUser $ selfProfile
responseJsonUnsafe <$> (post (b . path "/i/users" . json p) <!! const 201 === statusCode)

ephemeralUser :: HasCallStack => TestM UserId
ephemeralUser = do
Expand Down