77
88-module (topic_permission_SUITE ).
99
10+ -include_lib (" proper/include/proper.hrl" ).
1011-include_lib (" eunit/include/eunit.hrl" ).
1112-include_lib (" amqp10_common/include/amqp10_framing.hrl" ).
1213-include_lib (" amqp_client/include/amqp_client.hrl" ).
@@ -26,7 +27,9 @@ groups() ->
2627 amqpl_cc_headers ,
2728 amqpl_bcc_headers ,
2829 topic_permission_database_access ,
29- topic_permission_checks
30+ topic_permission_checks ,
31+ topic_permission_khepri_error_fails_closed ,
32+ topic_permission_khepri_error_fails_closed_prop
3033 ]}
3134 ].
3235
@@ -393,6 +396,105 @@ topic_permission_checks1(_Config) ->
393396
394397 ok .
395398
399+ % % Topic permission checks must fail closed on metadata store errors.
400+ topic_permission_khepri_error_fails_closed (Config ) ->
401+ ok = rabbit_ct_broker_helpers :rpc (Config , 0 ,
402+ ? MODULE , topic_permission_khepri_error_fails_closed1 , [Config ]).
403+
404+ topic_permission_khepri_error_fails_closed1 (_Config ) ->
405+ clear_tables (),
406+ rabbit_vhost :add (<<" /" >>, <<" acting-user" >>),
407+ rabbit_auth_backend_internal :add_user (<<" guest" >>, <<" guest" >>, <<" acting-user" >>),
408+ ok = rabbit_auth_backend_internal :set_topic_permissions (
409+ <<" guest" >>, <<" /" >>, <<" amq.topic" >>, " ^a" , " ^a" , <<" acting-user" >>),
410+
411+ User = # auth_user {username = <<" guest" >>},
412+ Topic = # resource {name = <<" amq.topic" >>, virtual_host = <<" /" >>,
413+ kind = topic },
414+ Context = #{routing_key => <<" secret.key" >>},
415+
416+ % % Baseline: routing key does not match "^a", so access is denied
417+ false = rabbit_auth_backend_internal :check_topic_access (
418+ User , Topic , write , Context ),
419+
420+ % % Mock rabbit_khepri:get to simulate a Khepri timeout
421+ ok = meck :new (rabbit_khepri , [passthrough ]),
422+ meck :expect (rabbit_khepri , get ,
423+ fun (_Path ) -> {error , timeout } end ),
424+ try
425+ % % Must return {error, timeout}, not 'true'
426+ {error , timeout } = rabbit_auth_backend_internal :check_topic_access (
427+ User , Topic , write , Context ),
428+ % % Also test with noproc
429+ meck :expect (rabbit_khepri , get ,
430+ fun (_Path ) -> {error , noproc } end ),
431+ {error , noproc } = rabbit_auth_backend_internal :check_topic_access (
432+ User , Topic , write , Context )
433+ after
434+ meck :unload (rabbit_khepri )
435+ end ,
436+
437+ % % After unmocking, normal behavior resumes
438+ false = rabbit_auth_backend_internal :check_topic_access (
439+ User , Topic , write , Context ),
440+ true = rabbit_auth_backend_internal :check_topic_access (
441+ User , Topic , write , #{routing_key => <<" a.b.c" >>}),
442+ ok .
443+
444+ % % Property: for any error reason, check_topic_access must propagate it.
445+ topic_permission_khepri_error_fails_closed_prop (Config ) ->
446+ ok = rabbit_ct_broker_helpers :rpc (Config , 0 ,
447+ ? MODULE , topic_permission_khepri_error_fails_closed_prop1 , [Config ]).
448+
449+ topic_permission_khepri_error_fails_closed_prop1 (_Config ) ->
450+ clear_tables (),
451+ rabbit_vhost :add (<<" /" >>, <<" acting-user" >>),
452+ rabbit_auth_backend_internal :add_user (<<" guest" >>, <<" guest" >>, <<" acting-user" >>),
453+ ok = rabbit_auth_backend_internal :set_topic_permissions (
454+ <<" guest" >>, <<" /" >>, <<" amq.topic" >>, " ^a" , " ^a" , <<" acting-user" >>),
455+ ok = meck :new (rabbit_khepri , [passthrough ]),
456+ try
457+ Property = fun () -> prop_khepri_error_fails_closed () end ,
458+ rabbit_ct_proper_helpers :run_proper (Property , [], 100 )
459+ after
460+ meck :unload (rabbit_khepri )
461+ end ,
462+ ok .
463+
464+ prop_khepri_error_fails_closed () ->
465+ ? FORALL (
466+ {ErrorReason , RoutingKey , Permission },
467+ {khepri_error_reason (), topic_routing_key (), oneof ([read , write ])},
468+ begin
469+ meck :expect (rabbit_khepri , get ,
470+ fun (_Path ) -> {error , ErrorReason } end ),
471+ User = # auth_user {username = <<" guest" >>},
472+ Topic = # resource {name = <<" amq.topic" >>, virtual_host = <<" /" >>,
473+ kind = topic },
474+ Context = #{routing_key => RoutingKey },
475+ Result = rabbit_auth_backend_internal :check_topic_access (
476+ User , Topic , Permission , Context ),
477+ ? WHENFAIL (
478+ ct :pal (" FAIL: error=~p , routing_key=~p , perm=~p , result=~p " ,
479+ [ErrorReason , RoutingKey , Permission , Result ]),
480+ Result =:= {error , ErrorReason })
481+ end ).
482+
483+ khepri_error_reason () ->
484+ oneof ([timeout , noproc ,
485+ {timeout , {gen_server , call , [fake_pid ]}},
486+ {nodedown , 'node@host' },
487+ {no_more_servers_to_try , []},
488+ {badmatch , undefined }]).
489+
490+ topic_routing_key () ->
491+ oneof ([<<" a.b.c" >>, <<" secret.key" >>, <<" public.1" >>,
492+ <<" x.y.z" >>, <<" " >>,
493+ ? LET (Parts ,
494+ non_empty (list (oneof ([<<" a" >>, <<" b" >>, <<" secret" >>,
495+ <<" public" >>, <<" x" >>]))),
496+ iolist_to_binary (lists :join (<<" ." >>, Parts )))]).
497+
396498clear_tables () ->
397499 ok = rabbit_db_vhost :clear (),
398500 ok = rabbit_db_user :clear ().
0 commit comments