@@ -2182,3 +2182,187 @@ async fn test_scim_list_users_with_groups(cptestctx: &ControlPlaneTestContext) {
21822182 let user5 = find_user ( & users[ 4 ] . id ) ;
21832183 assert ! ( user5. groups. is_none( ) ) ;
21842184}
2185+
2186+ #[ nexus_test]
2187+ async fn test_scim_list_groups_with_members ( cptestctx : & ControlPlaneTestContext ) {
2188+ let client = & cptestctx. external_client ;
2189+ let nexus = & cptestctx. server . server_context ( ) . nexus ;
2190+ let opctx = OpContext :: for_tests (
2191+ cptestctx. logctx . log . new ( o ! ( ) ) ,
2192+ nexus. datastore ( ) . clone ( ) ,
2193+ ) ;
2194+
2195+ const SILO_NAME : & str = "saml-scim-silo" ;
2196+ create_silo ( & client, SILO_NAME , true , shared:: SiloIdentityMode :: SamlScim )
2197+ . await ;
2198+
2199+ grant_iam (
2200+ client,
2201+ & format ! ( "/v1/system/silos/{SILO_NAME}" ) ,
2202+ shared:: SiloRole :: Admin ,
2203+ opctx. authn . actor ( ) . unwrap ( ) . silo_user_id ( ) . unwrap ( ) ,
2204+ AuthnMode :: PrivilegedUser ,
2205+ )
2206+ . await ;
2207+
2208+ let created_token: views:: ScimClientBearerTokenValue =
2209+ object_create_no_body (
2210+ client,
2211+ & format ! ( "/v1/system/scim/tokens?silo={}" , SILO_NAME ) ,
2212+ )
2213+ . await ;
2214+
2215+ // Create 5 users
2216+ let mut users = Vec :: new ( ) ;
2217+ for i in 1 ..=5 {
2218+ let user: scim2_rs:: User = NexusRequest :: new (
2219+ RequestBuilder :: new ( client, Method :: POST , "/scim/v2/Users" )
2220+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2221+ . header (
2222+ http:: header:: AUTHORIZATION ,
2223+ format ! ( "Bearer oxide-scim-{}" , created_token. bearer_token) ,
2224+ )
2225+ . allow_non_dropshot_errors ( )
2226+ . raw_body ( Some (
2227+ serde_json:: to_string ( & serde_json:: json!( {
2228+ "userName" : format!( "user{}" , i) ,
2229+ "externalId" : format!( "user{}@example.com" , i) ,
2230+ } ) )
2231+ . unwrap ( ) ,
2232+ ) )
2233+ . expect_status ( Some ( StatusCode :: CREATED ) ) ,
2234+ )
2235+ . execute_and_parse_unwrap ( )
2236+ . await ;
2237+ users. push ( user) ;
2238+ }
2239+
2240+ // Create 3 groups with various membership patterns:
2241+ // - group1: user1, user2, user3
2242+ // - group2: user1, user4
2243+ // - group3: no members
2244+ let group1: scim2_rs:: Group = NexusRequest :: new (
2245+ RequestBuilder :: new ( client, Method :: POST , "/scim/v2/Groups" )
2246+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2247+ . header (
2248+ http:: header:: AUTHORIZATION ,
2249+ format ! ( "Bearer oxide-scim-{}" , created_token. bearer_token) ,
2250+ )
2251+ . allow_non_dropshot_errors ( )
2252+ . raw_body ( Some (
2253+ serde_json:: to_string ( & serde_json:: json!( {
2254+ "displayName" : "group1" ,
2255+ "externalId" : "[email protected] " , 2256+ "members" : [
2257+ { "value" : users[ 0 ] . id} ,
2258+ { "value" : users[ 1 ] . id} ,
2259+ { "value" : users[ 2 ] . id} ,
2260+ ] ,
2261+ } ) )
2262+ . unwrap ( ) ,
2263+ ) )
2264+ . expect_status ( Some ( StatusCode :: CREATED ) ) ,
2265+ )
2266+ . execute_and_parse_unwrap ( )
2267+ . await ;
2268+
2269+ let group2: scim2_rs:: Group = NexusRequest :: new (
2270+ RequestBuilder :: new ( client, Method :: POST , "/scim/v2/Groups" )
2271+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2272+ . header (
2273+ http:: header:: AUTHORIZATION ,
2274+ format ! ( "Bearer oxide-scim-{}" , created_token. bearer_token) ,
2275+ )
2276+ . allow_non_dropshot_errors ( )
2277+ . raw_body ( Some (
2278+ serde_json:: to_string ( & serde_json:: json!( {
2279+ "displayName" : "group2" ,
2280+ "externalId" : "[email protected] " , 2281+ "members" : [
2282+ { "value" : users[ 0 ] . id} ,
2283+ { "value" : users[ 3 ] . id} ,
2284+ ] ,
2285+ } ) )
2286+ . unwrap ( ) ,
2287+ ) )
2288+ . expect_status ( Some ( StatusCode :: CREATED ) ) ,
2289+ )
2290+ . execute_and_parse_unwrap ( )
2291+ . await ;
2292+
2293+ let group3: scim2_rs:: Group = NexusRequest :: new (
2294+ RequestBuilder :: new ( client, Method :: POST , "/scim/v2/Groups" )
2295+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2296+ . header (
2297+ http:: header:: AUTHORIZATION ,
2298+ format ! ( "Bearer oxide-scim-{}" , created_token. bearer_token) ,
2299+ )
2300+ . allow_non_dropshot_errors ( )
2301+ . raw_body ( Some (
2302+ serde_json:: to_string ( & serde_json:: json!( {
2303+ "displayName" : "group3" ,
2304+ "externalId" : "[email protected] " , 2305+ } ) )
2306+ . unwrap ( ) ,
2307+ ) )
2308+ . expect_status ( Some ( StatusCode :: CREATED ) ) ,
2309+ )
2310+ . execute_and_parse_unwrap ( )
2311+ . await ;
2312+
2313+ // List all groups and verify members
2314+ let response: scim2_rs:: ListResponse = NexusRequest :: new (
2315+ RequestBuilder :: new ( client, Method :: GET , "/scim/v2/Groups" )
2316+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2317+ . header (
2318+ http:: header:: AUTHORIZATION ,
2319+ format ! ( "Bearer {}" , created_token. bearer_token) ,
2320+ )
2321+ . allow_non_dropshot_errors ( )
2322+ . expect_status ( Some ( StatusCode :: OK ) ) ,
2323+ )
2324+ . execute_and_parse_unwrap ( )
2325+ . await ;
2326+
2327+ let returned_groups: Vec < scim2_rs:: Group > = serde_json:: from_value (
2328+ serde_json:: to_value ( & response. resources ) . unwrap ( ) ,
2329+ )
2330+ . unwrap ( ) ;
2331+
2332+ // Find our created groups in the response
2333+ let find_group = |group_id : & str | {
2334+ returned_groups
2335+ . iter ( )
2336+ . find ( |g| g. id == group_id)
2337+ . expect ( "group should be in list" )
2338+ } ;
2339+
2340+ // group1 should have 3 members
2341+ let returned_group1 = find_group ( & group1. id ) ;
2342+ assert ! ( returned_group1. members. is_some( ) ) ;
2343+ let group1_members = returned_group1. members . as_ref ( ) . unwrap ( ) ;
2344+ assert_eq ! ( group1_members. len( ) , 3 ) ;
2345+ let group1_member_ids: std:: collections:: HashSet < _ > = group1_members
2346+ . iter ( )
2347+ . map ( |m| m. value . as_ref ( ) . unwrap ( ) . as_str ( ) )
2348+ . collect ( ) ;
2349+ assert ! ( group1_member_ids. contains( users[ 0 ] . id. as_str( ) ) ) ;
2350+ assert ! ( group1_member_ids. contains( users[ 1 ] . id. as_str( ) ) ) ;
2351+ assert ! ( group1_member_ids. contains( users[ 2 ] . id. as_str( ) ) ) ;
2352+
2353+ // group2 should have 2 members
2354+ let returned_group2 = find_group ( & group2. id ) ;
2355+ assert ! ( returned_group2. members. is_some( ) ) ;
2356+ let group2_members = returned_group2. members . as_ref ( ) . unwrap ( ) ;
2357+ assert_eq ! ( group2_members. len( ) , 2 ) ;
2358+ let group2_member_ids: std:: collections:: HashSet < _ > = group2_members
2359+ . iter ( )
2360+ . map ( |m| m. value . as_ref ( ) . unwrap ( ) . as_str ( ) )
2361+ . collect ( ) ;
2362+ assert ! ( group2_member_ids. contains( users[ 0 ] . id. as_str( ) ) ) ;
2363+ assert ! ( group2_member_ids. contains( users[ 3 ] . id. as_str( ) ) ) ;
2364+
2365+ // group3 should have no members
2366+ let returned_group3 = find_group ( & group3. id ) ;
2367+ assert ! ( returned_group3. members. is_none( ) ) ;
2368+ }
0 commit comments