Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5d2a07b
Abstract UserGroupPage
eyeinsky Nov 17, 2025
86c8c70
Adjust test to expect members
eyeinsky Nov 17, 2025
0a1a700
Return user groups with members from Brig (stub implementation)
eyeinsky Nov 17, 2025
19cbb29
Refactor `getUserGroups` (no functional changes)
eyeinsky Nov 19, 2025
c35f033
Implement `getUserGroupsWithMembers`
eyeinsky Nov 19, 2025
b295edf
Add changelog entry
eyeinsky Nov 20, 2025
2622dc2
fixup! Implement `getUserGroupsWithMembers`
eyeinsky Nov 21, 2025
22a9c88
fixup! Implement `getUserGroupsWithMembers`
eyeinsky Nov 21, 2025
a1ff47d
fixup! Implement `getUserGroupsWithMembers`
eyeinsky Nov 21, 2025
6cc6c87
fixup! Implement `getUserGroupsWithMembers`
eyeinsky Nov 21, 2025
2bb3963
fixup! Implement `getUserGroupsWithMembers`
eyeinsky Nov 21, 2025
7f44f27
fixup! Implement `getUserGroupsWithMembers`
eyeinsky Nov 21, 2025
7271395
fixup! Implement `getUserGroupsWithMembers`
eyeinsky Nov 21, 2025
236e9a6
fixup! Adjust test to expect members
eyeinsky Nov 21, 2025
8387e21
fixup! Adjust test to expect members
eyeinsky Nov 21, 2025
d2b0c6e
fixup! Implement `getUserGroupsWithMembers`
eyeinsky Nov 21, 2025
713e835
fixup! Abstract UserGroupPage
eyeinsky Nov 21, 2025
abf57d8
fixup! Return user groups with members from Brig (stub implementation)
eyeinsky Nov 21, 2025
22e9044
fixup! Return user groups with members from Brig (stub implementation)
eyeinsky Nov 21, 2025
7f7d14b
fixup! Adjust test to expect members
eyeinsky Nov 21, 2025
51ca01e
Add mock interpreter
eyeinsky Nov 21, 2025
1304bcc
fixup! Add mock interpreter
eyeinsky Nov 21, 2025
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/search-scim-groups-members
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Return group members as part of the search result in SCIM groups.
18 changes: 18 additions & 0 deletions integration/test/API/Spar.hs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,24 @@ filterScimUserGroup domain token mbFilter = do
& scimCommonHeaders token
& maybe id (\f -> addQueryParams [("filter", f)]) mbFilter

mkScimGroup :: String -> [Value] -> Value
mkScimGroup name members =
object
[ "schemas" .= ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName" .= name,
"members" .= members
]

mkScimUser :: String -> Value
mkScimUser scimUserId =
object
[ "type" .= "User",
"$ref" .= "...", -- something like
-- "https://example.org/v2/scim/User/ea2e4bf0-aa5e-11f0-96ad-e776a606779b"?
-- but since we're just receiving this it's ok to ignore.
"value" .= scimUserId
]

-- | https://staging-nginz-https.zinfra.io/v12/api/swagger-ui/#/default/idp-create
createIdp :: (HasCallStack, MakesValue user) => user -> SAML.IdPMetadata -> App Response
createIdp user metadata = do
Expand Down
78 changes: 33 additions & 45 deletions integration/test/Test/Spar.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ import API.GalleyInternal (setTeamFeatureStatus)
import API.Spar
import API.SparInternal
import Control.Concurrent (threadDelay)
import Control.Lens (to, (?~), (^.))
import Control.Lens (to, (^.))
import qualified Data.Aeson as A
import qualified Data.Aeson.KeyMap as KeyMap
import qualified Data.Aeson.Lens as A
import qualified Data.Aeson.Types as A
import qualified Data.CaseInsensitive as CI
import Data.String.Conversions (cs)
Expand Down Expand Up @@ -383,8 +382,8 @@ testSparCreateScimTokenWithName = do
----------------------------------------------------------------------
-- scim group stuff

testSparScimCreateGetUserGroup :: (HasCallStack) => App ()
testSparScimCreateGetUserGroup = do
testSparScimCreateGetSearchUserGroup :: (HasCallStack) => App ()
testSparScimCreateGetSearchUserGroup = do
(owner, tid, _) <- createTeam OwnDomain 1
tok <- createScimTokenV6 owner def >>= \resp -> resp.json %. "token" >>= asString
assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" "disabled"
Expand Down Expand Up @@ -419,47 +418,36 @@ testSparScimCreateGetUserGroup = do

scimUserId <- mkMemberCandidate
scimUserId2 <- mkMemberCandidate

resp <- createScimUserGroup OwnDomain tok $ mkScimGroup "ze groop" [mkScimUser scimUserId, mkScimUser scimUserId2]
resp4 <- createScimUserGroup OwnDomain tok $ mkScimGroup "ze group" [mkScimUser scimUserId, mkScimUser scimUserId2]
assertSuccess resp

gid <- resp.json %. "id" & asString
resp2 <- getScimUserGroup OwnDomain tok gid
resp.json `shouldMatch` resp2.json

filterResp <- filterScimUserGroup OwnDomain tok $ Just "displayName co \"e gro\""
assertSuccess filterResp
filterResultJson <- filterResp.json
foundGroups <- filterResultJson %. "Resources" & asList
createdGroup1 <- resp.json
createdGroup2 <- resp4.json
foundGroups `shouldMatch` (map removeMembers [createdGroup1, createdGroup2])

filterResultJson %. "totalResults" `shouldMatchInt` 2
filterResultJson %. "itemsPerPage" `shouldMatchInt` 2
filterResultJson %. "startIndex" `shouldMatchInt` 1
where
removeMembers g = g & A.atKey (fromString "members") ?~ toJSON ([] :: [()])

mkScimGroup :: String -> [Value] -> Value
mkScimGroup name members =
object
[ "schemas" .= ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName" .= name,
"members" .= members
]

mkScimUser :: String -> Value
mkScimUser scimUserId =
object
[ "type" .= "User",
"$ref" .= "...", -- something like
-- "https://example.org/v2/scim/User/ea2e4bf0-aa5e-11f0-96ad-e776a606779b"?
-- but since we're just receiving this it's ok
-- to ignore.
"value" .= scimUserId
]
scimUserId3 <- mkMemberCandidate

respGroup1 <- createScimUserGroup OwnDomain tok $ mkScimGroup "a group" [mkScimUser scimUserId, mkScimUser scimUserId2]
respGroup2 <- createScimUserGroup OwnDomain tok $ mkScimGroup "another group" [mkScimUser scimUserId, mkScimUser scimUserId2]
respGroup3 <- createScimUserGroup OwnDomain tok $ mkScimGroup "yet another group" [mkScimUser scimUserId2, mkScimUser scimUserId3]

createdGroup1 <- respGroup1.json
createdGroup2 <- respGroup2.json
createdGroup3 <- respGroup3.json

-- Test getting a single SCIM group by id
gid <- respGroup1.json %. "id" & asString
gottenGroup1 <- getScimUserGroup OwnDomain tok gid
respGroup1.json `shouldMatch` gottenGroup1.json

-- Test filter (get in bulk) SCIM groups
-- 1. Match "group", results in finding all three groups created above.
filterScimUserGroup OwnDomain tok (Just "displayName co \"group\"") `bindResponse` \allThreeResp ->
(allThreeResp.json %. "Resources" & asList) `shouldMatchSet` [createdGroup1, createdGroup2, createdGroup3]

-- 2. Match "another group", results in finding "another group" and "yet another group".
filterScimUserGroup OwnDomain tok (Just "displayName co \"another group\"") `bindResponse` \justTwo ->
(justTwo.json %. "Resources" & asList) `shouldMatchSet` [createdGroup2, createdGroup3]

-- 3. Empty groups should have empty member list.
respGroup4 <- createScimUserGroup OwnDomain tok $ mkScimGroup "empty group" []
filterScimUserGroup OwnDomain tok (Just "displayName co \"empty group\"") `bindResponse` \foundResults -> do
singleEmptyGroup <- foundResults.json %. "Resources" >>= asList >>= assertOne
(singleEmptyGroup %. "members" & asList) `shouldMatch` ([] :: [Value])
respGroup4.json `shouldMatch` singleEmptyGroup

testSparScimUpdateUserGroup :: (HasCallStack) => App ()
testSparScimUpdateUserGroup = do
Expand Down
2 changes: 1 addition & 1 deletion libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ type GetGroupsInternal =
:> "user-groups"
:> Capture "tid" TeamId
:> QueryParam' [Optional, Strict] "nameContains" Text.Text
:> Get '[Servant.JSON] UserGroupPage
:> Get '[Servant.JSON] UserGroupPageWithMembers
)

type UpdateGroupInternal =
Expand Down
21 changes: 16 additions & 5 deletions libs/wire-api/src/Wire/API/UserGroup/Pagination.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,30 @@ import Wire.API.Pagination
import Wire.API.UserGroup
import Wire.Arbitrary as Arbitrary

data UserGroupPage = UserGroupPage
{ page :: [UserGroupMeta],
-- | User group without members
type UserGroupPage = UserGroupPage_ UserGroupMeta

-- | User group with members
type UserGroupPageWithMembers = UserGroupPage_ UserGroup

-- * User group pages

--

-- | User group pages with different types of user groups.
data UserGroupPage_ a = UserGroupPage
{ page :: [a],
total :: Int
}
deriving (Eq, Show, Generic)
deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema UserGroupPage
deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema (UserGroupPage_ a)

instance ToSchema UserGroupPage where
instance (ToSchema a) => ToSchema (UserGroupPage_ a) where
schema =
objectWithDocModifier "UserGroupPage" addPageDocs $
UserGroupPage
<$> page .= field "page" (array schema)
<*> total .= field "total" schema

instance Arbitrary UserGroupPage where
instance (Arbitrary a) => Arbitrary (UserGroupPage_ a) where
arbitrary = UserGroupPage <$> arbitrary <*> arbitrary
2 changes: 1 addition & 1 deletion libs/wire-subsystems/src/Wire/BrigAPIAccess.hs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ data BrigAPIAccess m a where
GetAccountsBy :: GetBy -> BrigAPIAccess m [User]
CreateGroupInternal :: ManagedBy -> TeamId -> Maybe UserId -> NewUserGroup -> BrigAPIAccess m (Either Wai.Error UserGroup)
GetGroupInternal :: TeamId -> UserGroupId -> Bool -> BrigAPIAccess m (Maybe UserGroup)
GetGroupsInternal :: TeamId -> Maybe Scim.Filter -> BrigAPIAccess m UserGroupPage
GetGroupsInternal :: TeamId -> Maybe Scim.Filter -> BrigAPIAccess m UserGroupPageWithMembers
UpdateGroup :: UpdateGroupInternalRequest -> BrigAPIAccess m (Either Wai.Error ())
DeleteGroupInternal :: ManagedBy -> TeamId -> UserGroupId -> BrigAPIAccess m (Either DeleteGroupManagedError ())

Expand Down
2 changes: 1 addition & 1 deletion libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ getGroupsInternal ::
(Member Rpc r, Member (Input Endpoint) r, Member (Error ParseException) r) =>
TeamId ->
Maybe Scim.Filter ->
Sem r UserGroupPage
Sem r UserGroupPageWithMembers
getGroupsInternal tid mbFilter = do
maybeDisplayName :: Maybe Text <- case mbFilter of
Just filter' -> case filter' of
Expand Down
9 changes: 2 additions & 7 deletions libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{-# LANGUAGE RecordWildCards #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2025 Wire Swiss GmbH <[email protected]>
Expand Down Expand Up @@ -143,12 +141,9 @@ scimGetUserGroupsImpl ::
Maybe Scim.Filter ->
Sem r (Scim.ListResponse (SCG.StoredGroup SparTag))
scimGetUserGroupsImpl tid mbFilter = do
UserGroupPage {page} :: UserGroupPage <- BrigAPI.getGroupsInternal tid mbFilter
UserGroupPage {page} :: UserGroupPageWithMembers <- BrigAPI.getGroupsInternal tid mbFilter
ScimSubsystemConfig scimBaseUri <- input
pure . Scim.fromList $ toStoredGroup scimBaseUri . userGroupFromMeta <$> page
where
userGroupFromMeta :: UserGroupMeta -> UserGroup
userGroupFromMeta UserGroup_ {..} = UserGroup_ {members = pure mempty, ..}
pure . Scim.fromList $ toStoredGroup scimBaseUri <$> page

scimUpdateUserGroupImpl ::
forall r.
Expand Down
1 change: 1 addition & 0 deletions libs/wire-subsystems/src/Wire/UserGroupStore.hs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ data UserGroupStore m a where
CreateUserGroup :: TeamId -> NewUserGroup -> ManagedBy -> UserGroupStore m UserGroup
GetUserGroup :: TeamId -> UserGroupId -> Bool -> UserGroupStore m (Maybe UserGroup)
GetUserGroups :: UserGroupPageRequest -> UserGroupStore m UserGroupPage
GetUserGroupsWithMembers :: UserGroupPageRequest -> UserGroupStore m UserGroupPageWithMembers
GetUserGroupsForConv :: ConvId -> UserGroupStore m (Vector UserGroup)
UpdateUserGroup :: TeamId -> UserGroupId -> UserGroupUpdate -> UserGroupStore m (Maybe ())
DeleteUserGroup :: TeamId -> UserGroupId -> UserGroupStore m (Maybe ())
Expand Down
Loading