@@ -1993,3 +1993,195 @@ async fn test_scim_user_admin_group_priv_conflict(
19931993 . await
19941994 . expect ( "expected 200" ) ;
19951995}
1996+
1997+ #[ nexus_test]
1998+ async fn test_scim_list_users_with_groups ( cptestctx : & ControlPlaneTestContext ) {
1999+ let client = & cptestctx. external_client ;
2000+ let nexus = & cptestctx. server . server_context ( ) . nexus ;
2001+ let opctx = OpContext :: for_tests (
2002+ cptestctx. logctx . log . new ( o ! ( ) ) ,
2003+ nexus. datastore ( ) . clone ( ) ,
2004+ ) ;
2005+
2006+ const SILO_NAME : & str = "saml-scim-silo" ;
2007+ create_silo ( & client, SILO_NAME , true , shared:: SiloIdentityMode :: SamlScim )
2008+ . await ;
2009+
2010+ grant_iam (
2011+ client,
2012+ & format ! ( "/v1/system/silos/{SILO_NAME}" ) ,
2013+ shared:: SiloRole :: Admin ,
2014+ opctx. authn . actor ( ) . unwrap ( ) . silo_user_id ( ) . unwrap ( ) ,
2015+ AuthnMode :: PrivilegedUser ,
2016+ )
2017+ . await ;
2018+
2019+ let created_token: views:: ScimClientBearerTokenValue =
2020+ object_create_no_body (
2021+ client,
2022+ & format ! ( "/v1/system/scim/tokens?silo={}" , SILO_NAME ) ,
2023+ )
2024+ . await ;
2025+
2026+ // Create 5 users
2027+ let mut users = Vec :: new ( ) ;
2028+ for i in 1 ..=5 {
2029+ let user: scim2_rs:: User = NexusRequest :: new (
2030+ RequestBuilder :: new ( client, Method :: POST , "/scim/v2/Users" )
2031+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2032+ . header (
2033+ http:: header:: AUTHORIZATION ,
2034+ format ! ( "Bearer oxide-scim-{}" , created_token. bearer_token) ,
2035+ )
2036+ . allow_non_dropshot_errors ( )
2037+ . raw_body ( Some (
2038+ serde_json:: to_string ( & serde_json:: json!( {
2039+ "userName" : format!( "user{}" , i) ,
2040+ "externalId" : format!( "user{}@example.com" , i) ,
2041+ } ) )
2042+ . unwrap ( ) ,
2043+ ) )
2044+ . expect_status ( Some ( StatusCode :: CREATED ) ) ,
2045+ )
2046+ . execute_and_parse_unwrap ( )
2047+ . await ;
2048+ users. push ( user) ;
2049+ }
2050+
2051+ // Create 3 groups with various membership patterns:
2052+ // - group1: user1, user2, user3
2053+ // - group2: user1, user4
2054+ // - group3: no members
2055+ let group1: scim2_rs:: Group = NexusRequest :: new (
2056+ RequestBuilder :: new ( client, Method :: POST , "/scim/v2/Groups" )
2057+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2058+ . header (
2059+ http:: header:: AUTHORIZATION ,
2060+ format ! ( "Bearer oxide-scim-{}" , created_token. bearer_token) ,
2061+ )
2062+ . allow_non_dropshot_errors ( )
2063+ . raw_body ( Some (
2064+ serde_json:: to_string ( & serde_json:: json!( {
2065+ "displayName" : "group1" ,
2066+ "externalId" : "[email protected] " , 2067+ "members" : [
2068+ { "value" : users[ 0 ] . id} ,
2069+ { "value" : users[ 1 ] . id} ,
2070+ { "value" : users[ 2 ] . id} ,
2071+ ] ,
2072+ } ) )
2073+ . unwrap ( ) ,
2074+ ) )
2075+ . expect_status ( Some ( StatusCode :: CREATED ) ) ,
2076+ )
2077+ . execute_and_parse_unwrap ( )
2078+ . await ;
2079+
2080+ let group2: scim2_rs:: Group = NexusRequest :: new (
2081+ RequestBuilder :: new ( client, Method :: POST , "/scim/v2/Groups" )
2082+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2083+ . header (
2084+ http:: header:: AUTHORIZATION ,
2085+ format ! ( "Bearer oxide-scim-{}" , created_token. bearer_token) ,
2086+ )
2087+ . allow_non_dropshot_errors ( )
2088+ . raw_body ( Some (
2089+ serde_json:: to_string ( & serde_json:: json!( {
2090+ "displayName" : "group2" ,
2091+ "externalId" : "[email protected] " , 2092+ "members" : [
2093+ { "value" : users[ 0 ] . id} ,
2094+ { "value" : users[ 3 ] . id} ,
2095+ ] ,
2096+ } ) )
2097+ . unwrap ( ) ,
2098+ ) )
2099+ . expect_status ( Some ( StatusCode :: CREATED ) ) ,
2100+ )
2101+ . execute_and_parse_unwrap ( )
2102+ . await ;
2103+
2104+ let _group3: scim2_rs:: Group = NexusRequest :: new (
2105+ RequestBuilder :: new ( client, Method :: POST , "/scim/v2/Groups" )
2106+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2107+ . header (
2108+ http:: header:: AUTHORIZATION ,
2109+ format ! ( "Bearer oxide-scim-{}" , created_token. bearer_token) ,
2110+ )
2111+ . allow_non_dropshot_errors ( )
2112+ . raw_body ( Some (
2113+ serde_json:: to_string ( & serde_json:: json!( {
2114+ "displayName" : "group3" ,
2115+ "externalId" : "[email protected] " , 2116+ } ) )
2117+ . unwrap ( ) ,
2118+ ) )
2119+ . expect_status ( Some ( StatusCode :: CREATED ) ) ,
2120+ )
2121+ . execute_and_parse_unwrap ( )
2122+ . await ;
2123+
2124+ // List all users and verify group memberships
2125+ let response: scim2_rs:: ListResponse = NexusRequest :: new (
2126+ RequestBuilder :: new ( client, Method :: GET , "/scim/v2/Users" )
2127+ . header ( http:: header:: CONTENT_TYPE , "application/scim+json" )
2128+ . header (
2129+ http:: header:: AUTHORIZATION ,
2130+ format ! ( "Bearer oxide-scim-{}" , created_token. bearer_token) ,
2131+ )
2132+ . allow_non_dropshot_errors ( )
2133+ . expect_status ( Some ( StatusCode :: OK ) ) ,
2134+ )
2135+ . execute_and_parse_unwrap ( )
2136+ . await ;
2137+
2138+ let returned_users: Vec < scim2_rs:: User > = serde_json:: from_value (
2139+ serde_json:: to_value ( & response. resources ) . unwrap ( ) ,
2140+ )
2141+ . unwrap ( ) ;
2142+
2143+ // Find our created users in the response
2144+ let find_user = |user_id : & str | {
2145+ returned_users
2146+ . iter ( )
2147+ . find ( |u| u. id == user_id)
2148+ . expect ( "user should be in list" )
2149+ } ;
2150+
2151+ // user1 should be in group1 and group2
2152+ let user1 = find_user ( & users[ 0 ] . id ) ;
2153+ assert ! ( user1. groups. is_some( ) ) ;
2154+ let user1_groups = user1. groups . as_ref ( ) . unwrap ( ) ;
2155+ assert_eq ! ( user1_groups. len( ) , 2 ) ;
2156+ let user1_group_ids: std:: collections:: HashSet < _ > = user1_groups
2157+ . iter ( )
2158+ . map ( |g| g. value . as_ref ( ) . unwrap ( ) . as_str ( ) )
2159+ . collect ( ) ;
2160+ assert ! ( user1_group_ids. contains( group1. id. as_str( ) ) ) ;
2161+ assert ! ( user1_group_ids. contains( group2. id. as_str( ) ) ) ;
2162+
2163+ // user2 should be in group1 only
2164+ let user2 = find_user ( & users[ 1 ] . id ) ;
2165+ assert ! ( user2. groups. is_some( ) ) ;
2166+ let user2_groups = user2. groups . as_ref ( ) . unwrap ( ) ;
2167+ assert_eq ! ( user2_groups. len( ) , 1 ) ;
2168+ assert_eq ! ( user2_groups[ 0 ] . value. as_ref( ) . unwrap( ) , & group1. id) ;
2169+
2170+ // user3 should be in group1 only
2171+ let user3 = find_user ( & users[ 2 ] . id ) ;
2172+ assert ! ( user3. groups. is_some( ) ) ;
2173+ let user3_groups = user3. groups . as_ref ( ) . unwrap ( ) ;
2174+ assert_eq ! ( user3_groups. len( ) , 1 ) ;
2175+ assert_eq ! ( user3_groups[ 0 ] . value. as_ref( ) . unwrap( ) , & group1. id) ;
2176+
2177+ // user4 should be in group2 only
2178+ let user4 = find_user ( & users[ 3 ] . id ) ;
2179+ assert ! ( user4. groups. is_some( ) ) ;
2180+ let user4_groups = user4. groups . as_ref ( ) . unwrap ( ) ;
2181+ assert_eq ! ( user4_groups. len( ) , 1 ) ;
2182+ assert_eq ! ( user4_groups[ 0 ] . value. as_ref( ) . unwrap( ) , & group2. id) ;
2183+
2184+ // user5 should have no groups
2185+ let user5 = find_user ( & users[ 4 ] . id ) ;
2186+ assert ! ( user5. groups. is_none( ) ) ;
2187+ }
0 commit comments