@@ -1796,3 +1796,118 @@ fn remote_replica_namespace_header_support() {
17961796
17971797 sim. run ( ) . unwrap ( ) ;
17981798}
1799+
1800+ #[ test]
1801+ fn replicate_cross_namespace_denied ( ) {
1802+ init_tracing ( ) ;
1803+ let mut sim = Builder :: new ( )
1804+ . simulation_duration ( Duration :: from_secs ( 1000 ) )
1805+ . build ( ) ;
1806+
1807+ let ( encoding, decoding) = common:: auth:: key_pair ( ) ;
1808+ sim. host ( "primary" , {
1809+ let decoding = decoding. clone ( ) ;
1810+ move || {
1811+ let decoding = decoding. clone ( ) ;
1812+ async move {
1813+ let tmp = tempdir ( ) ?;
1814+ let jwt_keys =
1815+ vec ! [ jsonwebtoken:: DecodingKey :: from_ed_components( & decoding) . unwrap( ) ] ;
1816+ let auth = Auth :: new ( user_auth_strategies:: Jwt :: new ( jwt_keys) ) ;
1817+ let server = TestServer {
1818+ path : tmp. path ( ) . to_owned ( ) . into ( ) ,
1819+ user_api_config : UserApiConfig {
1820+ hrana_ws_acceptor : None ,
1821+ auth_strategy : auth,
1822+ ..Default :: default ( )
1823+ } ,
1824+ admin_api_config : Some ( AdminApiConfig {
1825+ acceptor : TurmoilAcceptor :: bind ( ( [ 0 , 0 , 0 , 0 ] , 9090 ) ) . await ?,
1826+ connector : TurmoilConnector ,
1827+ disable_metrics : true ,
1828+ auth_key : None ,
1829+ } ) ,
1830+ rpc_server_config : Some ( RpcServerConfig {
1831+ acceptor : TurmoilAcceptor :: bind ( ( [ 0 , 0 , 0 , 0 ] , 4567 ) ) . await ?,
1832+ tls_config : None ,
1833+ } ) ,
1834+ disable_namespaces : false ,
1835+ disable_default_namespace : true ,
1836+ ..Default :: default ( )
1837+ } ;
1838+
1839+ server. start_sim ( 8080 ) . await ?;
1840+
1841+ Ok ( ( ) )
1842+ }
1843+ }
1844+ } ) ;
1845+
1846+ sim. client ( "client" , async move {
1847+ // Create two distinct namespaces.
1848+ let client = Client :: new ( ) ;
1849+ client
1850+ . post ( "http://primary:9090/v1/namespaces/ns1/create" , json ! ( { } ) )
1851+ . await ?;
1852+ client
1853+ . post ( "http://primary:9090/v1/namespaces/ns2/create" , json ! ( { } ) )
1854+ . await ?;
1855+
1856+ // Token scoped to ns1 only.
1857+ let ns1_token = encode (
1858+ & serde_json:: json!( {
1859+ "p" : { "ro" : { "ns" : [ "ns1" ] } } ,
1860+ } ) ,
1861+ & encoding,
1862+ ) ;
1863+
1864+ // Sanity: ns1 token works against ns1.
1865+ let tmp = tempdir ( ) . unwrap ( ) ;
1866+ let db = Database :: open_with_remote_sync_connector (
1867+ tmp. path ( ) . join ( "embedded-ns1" ) . to_str ( ) . unwrap ( ) ,
1868+ "http://ns1.primary:8080" ,
1869+ ns1_token. clone ( ) ,
1870+ TurmoilConnector ,
1871+ false ,
1872+ None ,
1873+ )
1874+ . await ?;
1875+ assert ! ( db. sync( ) . await . is_ok( ) ) ;
1876+
1877+ // Negative: ns1 token must NOT replicate ns2.
1878+ let tmp = tempdir ( ) . unwrap ( ) ;
1879+ let db = Database :: open_with_remote_sync_connector (
1880+ tmp. path ( ) . join ( "embedded-ns2" ) . to_str ( ) . unwrap ( ) ,
1881+ "http://ns2.primary:8080" ,
1882+ ns1_token. clone ( ) ,
1883+ TurmoilConnector ,
1884+ false ,
1885+ None ,
1886+ )
1887+ . await ?;
1888+ assert ! ( db. sync( ) . await . is_err( ) ) ;
1889+
1890+ // Token scoped to both namespaces can replicate both (control).
1891+ let both_token = encode (
1892+ & serde_json:: json!( {
1893+ "p" : { "ro" : { "ns" : [ "ns1" , "ns2" ] } } ,
1894+ } ) ,
1895+ & encoding,
1896+ ) ;
1897+ let tmp = tempdir ( ) . unwrap ( ) ;
1898+ let db = Database :: open_with_remote_sync_connector (
1899+ tmp. path ( ) . join ( "embedded-ns2-both" ) . to_str ( ) . unwrap ( ) ,
1900+ "http://ns2.primary:8080" ,
1901+ both_token. clone ( ) ,
1902+ TurmoilConnector ,
1903+ false ,
1904+ None ,
1905+ )
1906+ . await ?;
1907+ assert ! ( db. sync( ) . await . is_ok( ) ) ;
1908+
1909+ Ok ( ( ) )
1910+ } ) ;
1911+
1912+ sim. run ( ) . unwrap ( ) ;
1913+ }
0 commit comments