@@ -12,7 +12,8 @@ use nexus_db_queries::db::fixed_data::silo::DEFAULT_SILO;
12
12
use nexus_db_queries:: db:: identity:: { Asset , Resource } ;
13
13
use nexus_test_utils:: http_testing:: TestResponse ;
14
14
use nexus_test_utils:: resource_helpers:: {
15
- object_delete_error, object_get, object_put, object_put_error,
15
+ create_local_user, object_delete_error, object_get, object_put,
16
+ object_put_error, test_params,
16
17
} ;
17
18
use nexus_test_utils:: {
18
19
http_testing:: { AuthnMode , NexusRequest , RequestBuilder } ,
@@ -28,7 +29,7 @@ use nexus_types::external_api::{
28
29
} ;
29
30
30
31
use http:: { StatusCode , header, method:: Method } ;
31
- use oxide_client:: types:: SiloRole ;
32
+ use oxide_client:: types:: { FleetRole , SiloRole } ;
32
33
use serde:: Deserialize ;
33
34
use tokio:: time:: { Duration , sleep} ;
34
35
use uuid:: Uuid ;
@@ -245,6 +246,7 @@ async fn test_device_auth_flow(cptestctx: &ControlPlaneTestContext) {
245
246
/// as a string
246
247
async fn get_device_token (
247
248
testctx : & ClientTestContext ,
249
+ authn_mode : AuthnMode ,
248
250
) -> DeviceAccessTokenGrant {
249
251
let client_id = Uuid :: new_v4 ( ) ;
250
252
let authn_params = DeviceAuthRequest { client_id, ttl_seconds : None } ;
@@ -272,7 +274,7 @@ async fn get_device_token(
272
274
. body ( Some ( & confirm_params) )
273
275
. expect_status ( Some ( StatusCode :: NO_CONTENT ) ) ,
274
276
)
275
- . authn_as ( AuthnMode :: PrivilegedUser )
277
+ . authn_as ( authn_mode . clone ( ) )
276
278
. execute ( )
277
279
. await
278
280
. expect ( "failed to confirm" ) ;
@@ -290,7 +292,7 @@ async fn get_device_token(
290
292
. body_urlencoded ( Some ( & token_params) )
291
293
. expect_status ( Some ( StatusCode :: OK ) ) ,
292
294
)
293
- . authn_as ( AuthnMode :: PrivilegedUser )
295
+ . authn_as ( authn_mode )
294
296
. execute ( )
295
297
. await
296
298
. expect ( "failed to get token" )
@@ -311,7 +313,8 @@ async fn test_device_token_expiration(cptestctx: &ControlPlaneTestContext) {
311
313
312
314
// get a token for the privileged user. default silo max token expiration
313
315
// is null, so tokens don't expire
314
- let initial_token_grant = get_device_token ( testctx) . await ;
316
+ let initial_token_grant =
317
+ get_device_token ( testctx, AuthnMode :: PrivilegedUser ) . await ;
315
318
let initial_token = initial_token_grant. access_token ;
316
319
317
320
// now there is a token in the list
@@ -381,7 +384,8 @@ async fn test_device_token_expiration(cptestctx: &ControlPlaneTestContext) {
381
384
assert_eq ! ( settings. device_token_max_ttl_seconds, Some ( 3 ) ) ;
382
385
383
386
// create token again (this one will have the 3-second expiration)
384
- let expiring_token_grant = get_device_token ( testctx) . await ;
387
+ let expiring_token_grant =
388
+ get_device_token ( testctx, AuthnMode :: PrivilegedUser ) . await ;
385
389
386
390
// check that expiration time is there and in the right range
387
391
let exp = expiring_token_grant
@@ -624,11 +628,142 @@ async fn test_device_token_request_ttl(cptestctx: &ControlPlaneTestContext) {
624
628
. expect ( "token should be expired" ) ;
625
629
}
626
630
631
+ #[ nexus_test]
632
+ async fn test_admin_logout_deletes_tokens ( cptestctx : & ControlPlaneTestContext ) {
633
+ let testctx = & cptestctx. external_client ;
634
+
635
+ // create a user have a user ID on hand to use in the authn_as
636
+ let silo_url = "/v1/system/silos/test-suite-silo" ;
637
+ let test_suite_silo: views:: Silo = object_get ( testctx, silo_url) . await ;
638
+ let user1 = create_local_user (
639
+ testctx,
640
+ & test_suite_silo,
641
+ & "user1" . parse ( ) . unwrap ( ) ,
642
+ test_params:: UserPassword :: LoginDisallowed ,
643
+ )
644
+ . await ;
645
+ let user2 = create_local_user (
646
+ testctx,
647
+ & test_suite_silo,
648
+ & "user2" . parse ( ) . unwrap ( ) ,
649
+ test_params:: UserPassword :: LoginDisallowed ,
650
+ )
651
+ . await ;
652
+
653
+ // TODO: we are using the fetch my tokens endpoint, authed as user1, to
654
+ // check the tokens, but we will likely have a list tokens for user endpoint
655
+ // (accessible to silo admins only) so they can feel good about there being
656
+ // no tokens or sessions for a given user
657
+
658
+ // no tokens for user 1 yet
659
+ let tokens = get_tokens_as ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
660
+ assert ! ( tokens. is_empty( ) ) ;
661
+
662
+ // create a token for user1
663
+ get_device_token ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
664
+
665
+ // now there is a token for user1
666
+ let tokens = get_tokens_as ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
667
+ assert_eq ! ( tokens. len( ) , 1 ) ;
668
+
669
+ let logout_url = format ! ( "/v1/users/{}/logout" , user1. id) ;
670
+
671
+ // user 2 cannot hit the logout endpoint for user 1
672
+ NexusRequest :: new (
673
+ RequestBuilder :: new ( testctx, Method :: POST , & logout_url)
674
+ . body ( Some ( & serde_json:: json!( { } ) ) )
675
+ . expect_status ( Some ( StatusCode :: FORBIDDEN ) ) ,
676
+ )
677
+ . authn_as ( AuthnMode :: SiloUser ( user2. id ) )
678
+ . execute ( )
679
+ . await
680
+ . expect ( "User has no perms, can't delete another user's tokens" ) ;
681
+
682
+ let tokens = get_tokens_as ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
683
+ assert_eq ! ( tokens. len( ) , 1 ) ;
684
+
685
+ // user 1 can hit the logout endpoint for themselves
686
+ NexusRequest :: new (
687
+ RequestBuilder :: new ( testctx, Method :: POST , & logout_url)
688
+ . body ( Some ( & serde_json:: json!( { } ) ) )
689
+ . expect_status ( Some ( StatusCode :: NO_CONTENT ) ) ,
690
+ )
691
+ . authn_as ( AuthnMode :: SiloUser ( user1. id ) )
692
+ . execute ( )
693
+ . await
694
+ . expect ( "User 1 should be able to delete their own tokens" ) ;
695
+
696
+ let tokens = get_tokens_as ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
697
+ assert ! ( tokens. is_empty( ) ) ;
698
+
699
+ // create another couple of tokens for user1
700
+ get_device_token ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
701
+ get_device_token ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
702
+
703
+ let tokens = get_tokens_as ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
704
+ assert_eq ! ( tokens. len( ) , 2 ) ;
705
+
706
+ // make user 2 fleet admin to show that fleet admin does not inherit
707
+ // the appropriate role due to being fleet admin alone
708
+ grant_iam (
709
+ testctx,
710
+ "/v1/system" ,
711
+ FleetRole :: Admin ,
712
+ user2. id ,
713
+ AuthnMode :: PrivilegedUser ,
714
+ )
715
+ . await ;
716
+
717
+ NexusRequest :: new (
718
+ RequestBuilder :: new ( testctx, Method :: POST , & logout_url)
719
+ . body ( Some ( & serde_json:: json!( { } ) ) )
720
+ . expect_status ( Some ( StatusCode :: FORBIDDEN ) ) ,
721
+ )
722
+ . authn_as ( AuthnMode :: SiloUser ( user2. id ) )
723
+ . execute ( )
724
+ . await
725
+ . expect ( "Fleet admin is not sufficient to delete another user's tokens" ) ;
726
+
727
+ let tokens = get_tokens_as ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
728
+ assert_eq ! ( tokens. len( ) , 2 ) ;
729
+
730
+ // make user 2 a silo admin so they can delete user 1's tokens
731
+ grant_iam (
732
+ testctx,
733
+ silo_url,
734
+ SiloRole :: Admin ,
735
+ user2. id ,
736
+ AuthnMode :: PrivilegedUser ,
737
+ )
738
+ . await ;
739
+
740
+ NexusRequest :: new (
741
+ RequestBuilder :: new ( testctx, Method :: POST , & logout_url)
742
+ . body ( Some ( & serde_json:: json!( { } ) ) )
743
+ . expect_status ( Some ( StatusCode :: NO_CONTENT ) ) ,
744
+ )
745
+ . authn_as ( AuthnMode :: SiloUser ( user1. id ) )
746
+ . execute ( )
747
+ . await
748
+ . expect ( "Silo admin should be able to delete user 1's tokens" ) ;
749
+
750
+ // they're gone!
751
+ let tokens = get_tokens_as ( testctx, AuthnMode :: SiloUser ( user1. id ) ) . await ;
752
+ assert ! ( tokens. is_empty( ) ) ;
753
+ }
754
+
627
755
async fn get_tokens_priv (
628
756
testctx : & ClientTestContext ,
757
+ ) -> Vec < views:: DeviceAccessToken > {
758
+ get_tokens_as ( testctx, AuthnMode :: PrivilegedUser ) . await
759
+ }
760
+
761
+ async fn get_tokens_as (
762
+ testctx : & ClientTestContext ,
763
+ authn_mode : AuthnMode ,
629
764
) -> Vec < views:: DeviceAccessToken > {
630
765
NexusRequest :: object_get ( testctx, "/v1/me/access-tokens" )
631
- . authn_as ( AuthnMode :: PrivilegedUser )
766
+ . authn_as ( authn_mode )
632
767
. execute_and_parse_unwrap :: < ResultsPage < views:: DeviceAccessToken > > ( )
633
768
. await
634
769
. items
0 commit comments