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