diff --git a/changelog.d/5-internal/WPB-25521-refactor_-make-team-conversation-access-control-more-collaborator-friendly b/changelog.d/5-internal/WPB-25521-refactor_-make-team-conversation-access-control-more-collaborator-friendly new file mode 100644 index 00000000000..0b8c9950b70 --- /dev/null +++ b/changelog.d/5-internal/WPB-25521-refactor_-make-team-conversation-access-control-more-collaborator-friendly @@ -0,0 +1 @@ +Refactor: make team conversation access control more collaborator-friendly. diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 2dbe0c784dc..270e5cf991e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -2082,7 +2082,7 @@ type TeamsAPI = :> "teams" :> Capture "tid" TeamId :> "collaborators" - :> MultiVerb1 'GET '[JSON] (Respond 200 "Return collaborators" [TeamCollaborator]) + :> MultiVerb1 'GET '[JSON] (Respond 200 "Return collaborators" [TeamCollaboratorView]) ) type SystemSettingsAPI = diff --git a/libs/wire-api/src/Wire/API/Team/Collaborator.hs b/libs/wire-api/src/Wire/API/Team/Collaborator.hs index d256ce92474..7da61b03bff 100644 --- a/libs/wire-api/src/Wire/API/Team/Collaborator.hs +++ b/libs/wire-api/src/Wire/API/Team/Collaborator.hs @@ -74,3 +74,40 @@ instance ToSchema TeamCollaborator where <$> (gUser .= field "user" schema) <*> (gTeam .= field "team" schema) <*> (gPermissions .= field "permissions" (set schema)) + +data CollaboratorStatus = CollaboratorActive | CollaboratorPseudoSuspended + deriving (Eq, Ord, Show, Generic) + deriving (Arbitrary) via GenericUniform CollaboratorStatus + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema CollaboratorStatus) + +instance ToSchema CollaboratorStatus where + schema = + enum @Text $ + mconcat + [ element "active" CollaboratorActive, + element "pseudo_suspended" CollaboratorPseudoSuspended + ] + +-- | API response type for collaborators, enriched with a computed status field. +-- The status is not stored; it is derived server-side from the user type and +-- the team's feature configuration. +data TeamCollaboratorView = TeamCollaboratorView + { tcvUser :: UserId, + tcvTeam :: TeamId, + tcvPermissions :: Set CollaboratorPermission, + tcvStatus :: CollaboratorStatus + } + deriving (Eq, Show) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema TeamCollaboratorView) + +instance ToSchema TeamCollaboratorView where + schema = + object $ + TeamCollaboratorView + <$> (.tcvUser) .= field "user" schema + <*> (.tcvTeam) .= field "team" schema + <*> (.tcvPermissions) .= field "permissions" (set schema) + <*> (.tcvStatus) .= field "status" schema + +collaboratorToView :: CollaboratorStatus -> TeamCollaborator -> TeamCollaboratorView +collaboratorToView status c = TeamCollaboratorView c.gUser c.gTeam c.gPermissions status diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 7a0b09dbe07..a3ebf4bfd9e 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -71,6 +71,9 @@ module Wire.API.Team.Member IsPerm (..), HiddenPerm (..), mkSingleTeamMembersPage, + + -- * TeamPrincipal + TeamPrincipal, ) where @@ -657,6 +660,12 @@ collaboratorToTeamPermissions = Collaborator.ImplicitConnection -> mempty ) +-- | A user associated with a team, either as a collaborator (@Left@) or as a +-- full team member (@Right@). The 'IsPerm' instance is derived automatically +-- via the 'Either' instance, using 'collaboratorToTeamPermissions' for the +-- @Left@ case. +type TeamPrincipal = Either TeamCollaborator TeamMember + ---------------------------------------------------------------------- makeLenses ''TeamMember' diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Action.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Action.hs index 3c81ed700f9..b150c141b9b 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Action.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Action.hs @@ -136,6 +136,7 @@ import Wire.Sem.Now qualified as Now import Wire.Sem.Random (Random) import Wire.StoredConversation import Wire.StoredConversation qualified as Data +import Wire.API.Team.Collaborator (TeamCollaborator (..)) import Wire.TeamCollaboratorsSubsystem import Wire.TeamStore import Wire.TeamSubsystem (ConsentGiven (..), TeamSubsystem, consentGiven) @@ -189,6 +190,7 @@ instance IsConversationAction 'ConversationJoinTag where Member BackendNotificationQueueAccess r, Member TeamCollaboratorsSubsystem r, Member FederationSubsystem r, + Member FeaturesConfigSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, Member E.BrigAPIAccess r, @@ -794,7 +796,15 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do tms <- Map.fromList . map (view Wire.API.Team.Member.userId &&& Imports.id) <$> TeamSubsystem.internalSelectTeamMembers tid newUsers - let userMembershipMap = map (Imports.id &&& flip Map.lookup tms) newUsers + collabs <- + Map.fromList . map (\c -> (c.gUser, c)) + <$> internalGetTeamCollaboratorsWithIds (Set.singleton tid) (Set.fromList newUsers) + pseudoSusp <- TeamSubsystem.pseudoSuspendedCollaborators tid (Map.keys collabs) + let activeCollabs = Map.filterWithKey (\uid _ -> uid `Set.notMember` pseudoSusp) collabs + principalFor uid = + fmap Right (Map.lookup uid tms) + <|> fmap Left (Map.lookup uid activeCollabs) + userMembershipMap = map (Imports.id &&& principalFor) newUsers ensureAccessRole (convAccessRoles conv) userMembershipMap ensureConnectedToLocalsOrSameTeam lusr newUsers checkLocals lusr Nothing newUsers = do @@ -991,6 +1001,7 @@ updateLocalConversationJoin :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'ConvNotFound) r, Member FederationSubsystem r, + Member FeaturesConfigSubsystem r, Member TeamCollaboratorsSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, @@ -1315,6 +1326,7 @@ updateLocalConversationUncheckedJoin :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'ConvNotFound) r, Member FederationSubsystem r, + Member FeaturesConfigSubsystem r, Member TeamCollaboratorsSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r, diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/CreateInternal.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/CreateInternal.hs index e28425c9061..574e33010ea 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/CreateInternal.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/CreateInternal.hs @@ -55,7 +55,6 @@ import Wire.API.Team import Wire.API.Team.Collaborator qualified as CollaboratorPermission import Wire.API.Team.Feature import Wire.API.Team.Feature qualified as Conf -import Wire.API.Team.FeatureFlags (notTeamMember) import Wire.API.Team.LegalHold (LegalholdProtectee (LegalholdPlusFederationNotImplemented)) import Wire.API.Team.Member import Wire.API.Team.Permission hiding (self) @@ -330,11 +329,8 @@ checkCreateConvPermissions lusr newConv Nothing allUsers = do ensureConnected lusr allUsers checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do let convTeam = cnvTeamId tinfo - mTeamMember <- getTeamMember (tUnqualified lusr) (Just convTeam) - teamAssociation <- case mTeamMember of - Just tm -> pure (Just (Right tm)) - Nothing -> do - Left <$$> internalGetTeamCollaborator convTeam (tUnqualified lusr) + teamAssociation <- TeamSubsystem.lookupTeamPrincipal convTeam (tUnqualified lusr) + let mTeamMember = teamAssociation >>= either (const Nothing) Just let checkGroup = do void $ permissionCheck CreateConversation teamAssociation @@ -346,9 +342,10 @@ checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do GroupConversation -> checkGroup MeetingConversation -> checkGroup - convLocalMemberships <- mapM (flip TeamSubsystem.internalGetTeamMember convTeam) (ulLocals allUsers) - ensureAccessRole (accessRoles newConv) (zip (ulLocals allUsers) convLocalMemberships) - ensureConnectedToLocals (tUnqualified lusr) (notTeamMember (ulLocals allUsers) (catMaybes convLocalMemberships)) + convLocalMemberships <- mapM (TeamSubsystem.lookupTeamPrincipal convTeam) (ulLocals allUsers) + let allUsersWithPrincipal = zip (ulLocals allUsers) convLocalMemberships + ensureAccessRole (accessRoles newConv) allUsersWithPrincipal + ensureConnectedToLocals (tUnqualified lusr) [uid | (uid, Nothing) <- allUsersWithPrincipal] ensureConnectedToRemotes lusr (ulRemotes allUsers) where ensureCreateChannelPermissions :: diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs index 34a22f5dd4f..6f8431c6c87 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Federation.hs @@ -657,7 +657,8 @@ sendMLSMessage :: Member P.TinyLog r, Member ProposalStore r, Member TeamCollaboratorsSubsystem r, - Member TeamStore r + Member TeamStore r, + Member FeaturesConfigSubsystem r ) => Domain -> MLSMessageSendRequest -> diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/Core.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/Core.hs index a51f0adcbcd..ddd73809f11 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/Core.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Commit/Core.hs @@ -73,6 +73,7 @@ import Wire.NotificationSubsystem import Wire.ProposalStore (ProposalStore) import Wire.Sem.Now (Now) import Wire.Sem.Random (Random) +import Wire.FeaturesConfigSubsystem import Wire.TeamCollaboratorsSubsystem import Wire.TeamStore @@ -101,7 +102,8 @@ type HasProposalActionEffects r = Member TinyLog r, Member NotificationSubsystem r, Member Random r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member FeaturesConfigSubsystem r ) getCommitData :: diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs index 712fd32aee4..18400ee39b2 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/MLS/Proposal.hs @@ -73,6 +73,7 @@ import Wire.LegalHoldStore (LegalHoldStore) import Wire.NotificationSubsystem import Wire.ProposalStore import Wire.Sem.Now (Now) +import Wire.FeaturesConfigSubsystem import Wire.TeamCollaboratorsSubsystem import Wire.TeamStore import Wire.Util @@ -138,7 +139,8 @@ type HasProposalEffects r = Member ProposalStore r, Member TeamStore r, Member TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member FeaturesConfigSubsystem r ) derefOrCheckProposal :: diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs index ce8009d45bd..318571b58ca 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs @@ -645,9 +645,10 @@ getConversationByReusableCode :: Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'GuestLinksDisabled) r, Member (ErrorS 'NotATeamMember) r, - Member FeaturesConfigSubsystem r, Member HashPassword r, Member RateLimit r, + Member FeaturesConfigSubsystem r, + Member TeamCollaboratorsSubsystem r, Member TeamSubsystem r ) => Local UserId -> diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs index 96a798aeca3..85a9b38c896 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs @@ -741,6 +741,8 @@ joinConversationByReusableCode :: Member FeaturesConfigSubsystem r, Member HashPassword r, Member RateLimit r, + Member FeaturesConfigSubsystem r, + Member TeamCollaboratorsSubsystem r, Member TeamSubsystem r, Member Now r, Member (Input ConversationSubsystemConfig) r @@ -769,7 +771,9 @@ joinConversationById :: Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, Member E.ExternalAccess r, + Member FeaturesConfigSubsystem r, Member Now r, + Member TeamCollaboratorsSubsystem r, Member TeamSubsystem r ) => Local UserId -> @@ -792,8 +796,10 @@ joinConversation :: Member BackendNotificationQueueAccess r, Member E.ExternalAccess r, Member ConversationStore r, + Member FeaturesConfigSubsystem r, Member Now r, Member NotificationSubsystem r, + Member TeamCollaboratorsSubsystem r, Member TeamSubsystem r ) => Local UserId -> @@ -857,6 +863,7 @@ addMembers :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, + Member FeaturesConfigSubsystem r, Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r @@ -908,6 +915,7 @@ addQualifiedMembersUnqualified :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, + Member FeaturesConfigSubsystem r, Member FederationSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r @@ -955,6 +963,7 @@ replaceMembers :: Member TeamCollaboratorsSubsystem r, Member UserGroupStore r, Member FederationSubsystem r, + Member FeaturesConfigSubsystem r, Member TeamSubsystem r, Member (Input ConversationSubsystemConfig) r ) => diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Util.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Util.hs index 78d70193979..eab940d1e41 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Util.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Util.hs @@ -92,6 +92,7 @@ import Wire.RateLimit import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation as Data +import Wire.FeaturesConfigSubsystem import Wire.TeamCollaboratorsSubsystem import Wire.TeamStore import Wire.TeamSubsystem (ConsentGiven (..), TeamSubsystem, consentGiven, getLHStatus) @@ -115,7 +116,7 @@ ensureAccessRole :: Member (ErrorS 'ConvAccessDenied) r ) => Set Public.AccessRole -> - [(UserId, Maybe TeamMember {- isJust iff user and conv are in the same team -})] -> + [(UserId, Maybe TeamPrincipal {- Just (Right tm) iff full team member, Just (Left c) iff collaborator, Nothing otherwise -})] -> Sem r () ensureAccessRole roles users = do when (Set.null roles) $ throwS @'ConvAccessDenied @@ -676,6 +677,8 @@ ensureConversationAccess :: ( Member BrigAPIAccess r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'NotATeamMember) r, + Member FeaturesConfigSubsystem r, + Member TeamCollaboratorsSubsystem r, Member TeamSubsystem r ) => UserId -> @@ -684,8 +687,8 @@ ensureConversationAccess :: Sem r () ensureConversationAccess zusr conv access = do ensureAccess conv access - zusrMembership <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember zusr) (Data.convTeam conv) - ensureAccessRole (Data.convAccessRoles conv) [(zusr, zusrMembership)] + zusrPrincipal <- maybe (pure Nothing) (\tid -> TeamSubsystem.lookupTeamPrincipal tid zusr) (Data.convTeam conv) + ensureAccessRole (Data.convAccessRoles conv) [(zusr, zusrPrincipal)] ensureAccess :: (Member (ErrorS 'ConvAccessDenied) r) => diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem.hs index cd4fa9a7cac..ba464d952e2 100644 --- a/libs/wire-subsystems/src/Wire/TeamSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem.hs @@ -22,17 +22,24 @@ module Wire.TeamSubsystem where import Data.Id import Data.LegalHold import Data.Map qualified as Map +import Data.Proxy (Proxy (..)) import Data.Qualified import Data.Range +import Data.Set qualified as Set import Data.Singletons (Demote, Sing, SingKind, fromSing) import Imports import Polysemy import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Team.Feature (AppsConfig) import Wire.API.Team.LegalHold (UserLegalHoldStatusResponse) import Wire.API.Team.Member import Wire.API.Team.Member.Error import Wire.API.Team.Member.Info (TeamMemberInfoList) +import Wire.API.User (User (..), UserType (..)) +import Wire.BrigAPIAccess +import Wire.FeaturesConfigSubsystem +import Wire.TeamCollaboratorsSubsystem data PermissionCheckArgs teamAssociation where PermissionCheckArgs :: @@ -144,3 +151,55 @@ checkConsent :: Sem r ConsentGiven checkConsent teamsOfUsers other = do consentGiven <$> getLHStatus (Map.lookup other teamsOfUsers) other + +-- | Returns the set of user IDs from @uids@ that are pseudo-suspended as +-- collaborators in @tid@. A collaborator is pseudo-suspended when they are an +-- app user and the team's @apps@ feature is disabled. The feature is checked +-- once; user types are only fetched when the feature is off. +pseudoSuspendedCollaborators :: + ( Member BrigAPIAccess r, + Member FeaturesConfigSubsystem r + ) => + TeamId -> + [UserId] -> + Sem r (Set UserId) +pseudoSuspendedCollaborators _ [] = pure Set.empty +pseudoSuspendedCollaborators tid uids = do + appsEnabled <- featureEnabledForTeam (Proxy @AppsConfig) tid + if appsEnabled + then pure Set.empty + else do + users <- getUsers uids + pure $ Set.fromList [qUnqualified u.userQualifiedId | u <- users, u.userType == UserTypeApp] + +isPseudoSuspended :: + ( Member BrigAPIAccess r, + Member FeaturesConfigSubsystem r + ) => + TeamId -> + UserId -> + Sem r Bool +isPseudoSuspended tid uid = Set.member uid <$> pseudoSuspendedCollaborators tid [uid] + +-- | Look up a user as a 'TeamPrincipal': a full member (@Right@) takes +-- precedence over a collaborator (@Left@). Returns 'Nothing' if the user has +-- no association with the team, or if they are a pseudo-suspended collaborator +-- (an app user whose team has the @apps@ feature disabled). +lookupTeamPrincipal :: + ( Member TeamSubsystem r, + Member TeamCollaboratorsSubsystem r, + Member BrigAPIAccess r, + Member FeaturesConfigSubsystem r + ) => + TeamId -> + UserId -> + Sem r (Maybe TeamPrincipal) +lookupTeamPrincipal tid uid = + internalGetTeamMember uid tid >>= \case + Just m -> pure (Just (Right m)) + Nothing -> + internalGetTeamCollaborator tid uid >>= \case + Nothing -> pure Nothing + Just c -> do + pseudo <- isPseudoSuspended tid uid + pure $ if pseudo then Nothing else Just (Left c) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index fdde7fdf4d9..b6de187cd3f 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -38,6 +38,7 @@ import Control.Monad.Trans.Except import Data.ByteString.Conversion (toByteString) import Data.Id import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.Set qualified as Set import Data.Qualified import Data.Range import Data.Text.Encoding (encodeUtf8) @@ -56,12 +57,14 @@ import URI.ByteString (Absolute, URIRef, laxURIParserOptions, parseURI) import Util.Logging (logFunction, logTeam) import Wire.API.Error import Wire.API.Error.Brig -import Wire.API.Routes.Internal.Brig (FoundInvitationCode (FoundInvitationCode)) +import Wire.API.Routes.Internal.Brig (GetBy (..), FoundInvitationCode (FoundInvitationCode), getByNoFilters) import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named import Wire.API.Routes.Public.Brig (TeamsAPI) import Wire.API.Team import Wire.API.Team.Collaborator +import Wire.API.Team.Feature (AppsConfig, FeatureStatus (..), LockableFeature (..)) +import Wire.API.User (UserType (..)) import Wire.API.Team.Invitation import Wire.API.Team.Invitation qualified as Public import Wire.API.Team.Member (teamMembers) @@ -115,7 +118,36 @@ servantAPI = :<|> Named @"get-team-size" (\uid tid -> lift . liftSem $ teamSizePublic uid tid) :<|> Named @"accept-team-invitation" (\luid req -> lift $ liftSem $ acceptTeamInvitation luid req.password req.code) :<|> Named @"add-team-collaborator" (\zuid tid (NewTeamCollaborator uid perms) -> lift . liftSem $ createTeamCollaborator zuid uid tid perms) - :<|> Named @"get-team-collaborators" (\zuid tid -> lift . liftSem $ getAllTeamCollaborators zuid tid) + :<|> Named @"get-team-collaborators" (\zuidLocal tid -> lift . liftSem $ do + collabs <- getAllTeamCollaborators zuidLocal tid + enrichCollaboratorsWithStatus tid collabs) + +-- | Compute a 'CollaboratorStatus' for each collaborator. App-type collaborators +-- in a team whose @apps@ feature is disabled are returned as 'CollaboratorPseudoSuspended'. +enrichCollaboratorsWithStatus :: + ( Member GalleyAPIAccess r, + Member UserSubsystem r, + Member (Input (Local ())) r + ) => + TeamId -> + [TeamCollaborator] -> + Sem r [TeamCollaboratorView] +enrichCollaboratorsWithStatus _ [] = pure [] +enrichCollaboratorsWithStatus tid collabs = do + appsFeature <- GalleyAPIAccess.getFeatureConfigForTeam @_ @AppsConfig tid + if appsFeature.status == FeatureStatusEnabled + then pure (map (collaboratorToView CollaboratorActive) collabs) + else do + localUnit <- input @(Local ()) + let uids = map (.gUser) collabs + users <- getAccountsBy (qualifyAs localUnit (getByNoFilters {getByUserId = uids})) + let appUserIds = Set.fromList [qUnqualified u.userQualifiedId | u <- users, u.userType == UserTypeApp] + pure + [ collaboratorToView + (if c.gUser `Set.member` appUserIds then CollaboratorPseudoSuspended else CollaboratorActive) + c + | c <- collabs + ] teamSizePublic :: ( Member (Error UserSubsystemError) r,