@@ -13,6 +13,7 @@ import (
13
13
"testing"
14
14
"time"
15
15
16
+ "github.com/go-jose/go-jose/v4/jwt"
16
17
"github.com/google/uuid"
17
18
"github.com/stretchr/testify/assert"
18
19
"github.com/stretchr/testify/require"
@@ -37,6 +38,7 @@ import (
37
38
"github.com/coder/coder/v2/coderd/database/dbtime"
38
39
"github.com/coder/coder/v2/coderd/database/pubsub"
39
40
"github.com/coder/coder/v2/coderd/externalauth"
41
+ "github.com/coder/coder/v2/coderd/jwtutils"
40
42
"github.com/coder/coder/v2/codersdk"
41
43
"github.com/coder/coder/v2/codersdk/agentsdk"
42
44
"github.com/coder/coder/v2/codersdk/workspacesdk"
@@ -555,73 +557,136 @@ func (r *resumeTokenRecordingProvider) VerifyResumeToken(ctx context.Context, to
555
557
func TestWorkspaceAgentClientCoordinate_ResumeToken (t * testing.T ) {
556
558
t .Parallel ()
557
559
558
- logger := slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
559
- clock := quartz .NewMock (t )
560
- resumeTokenSigningKey , err := tailnet .GenerateResumeTokenSigningKey ()
561
- mgr := cryptokeys.StaticKey {
562
- ID : uuid .New ().String (),
563
- Key : resumeTokenSigningKey [:],
564
- }
565
- require .NoError (t , err )
566
- resumeTokenProvider := newResumeTokenRecordingProvider (
567
- t ,
568
- tailnet .NewResumeTokenKeyProvider (mgr , clock , time .Hour ),
569
- )
570
- client , closer , api := coderdtest .NewWithAPI (t , & coderdtest.Options {
571
- Coordinator : tailnet .NewCoordinator (logger ),
572
- CoordinatorResumeTokenProvider : resumeTokenProvider ,
560
+ t .Run ("OK" , func (t * testing.T ) {
561
+ t .Parallel ()
562
+
563
+ logger := slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
564
+ clock := quartz .NewMock (t )
565
+ resumeTokenSigningKey , err := tailnet .GenerateResumeTokenSigningKey ()
566
+ mgr := cryptokeys.StaticKey {
567
+ ID : uuid .New ().String (),
568
+ Key : resumeTokenSigningKey [:],
569
+ }
570
+ require .NoError (t , err )
571
+ resumeTokenProvider := newResumeTokenRecordingProvider (
572
+ t ,
573
+ tailnet .NewResumeTokenKeyProvider (mgr , clock , time .Hour ),
574
+ )
575
+ client , closer , api := coderdtest .NewWithAPI (t , & coderdtest.Options {
576
+ Coordinator : tailnet .NewCoordinator (logger ),
577
+ CoordinatorResumeTokenProvider : resumeTokenProvider ,
578
+ })
579
+ defer closer .Close ()
580
+ user := coderdtest .CreateFirstUser (t , client )
581
+
582
+ // Create a workspace with an agent. No need to connect it since clients can
583
+ // still connect to the coordinator while the agent isn't connected.
584
+ r := dbfake .WorkspaceBuild (t , api .Database , database.Workspace {
585
+ OrganizationID : user .OrganizationID ,
586
+ OwnerID : user .UserID ,
587
+ }).WithAgent ().Do ()
588
+ agentTokenUUID , err := uuid .Parse (r .AgentToken )
589
+ require .NoError (t , err )
590
+ ctx := testutil .Context (t , testutil .WaitLong )
591
+ agentAndBuild , err := api .Database .GetWorkspaceAgentAndLatestBuildByAuthToken (dbauthz .AsSystemRestricted (ctx ), agentTokenUUID ) //nolint
592
+ require .NoError (t , err )
593
+
594
+ // Connect with no resume token, and ensure that the peer ID is set to a
595
+ // random value.
596
+ originalResumeToken , err := connectToCoordinatorAndFetchResumeToken (ctx , logger , client , agentAndBuild .WorkspaceAgent .ID , "" )
597
+ require .NoError (t , err )
598
+ originalPeerID := testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .generateCalls )
599
+ require .NotEqual (t , originalPeerID , uuid .Nil )
600
+
601
+ // Connect with a valid resume token, and ensure that the peer ID is set to
602
+ // the stored value.
603
+ clock .Advance (time .Second )
604
+ newResumeToken , err := connectToCoordinatorAndFetchResumeToken (ctx , logger , client , agentAndBuild .WorkspaceAgent .ID , originalResumeToken )
605
+ require .NoError (t , err )
606
+ verifiedToken := testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .verifyCalls )
607
+ require .Equal (t , originalResumeToken , verifiedToken )
608
+ newPeerID := testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .generateCalls )
609
+ require .Equal (t , originalPeerID , newPeerID )
610
+ require .NotEqual (t , originalResumeToken , newResumeToken )
611
+
612
+ // Connect with an invalid resume token, and ensure that the request is
613
+ // rejected.
614
+ clock .Advance (time .Second )
615
+ _ , err = connectToCoordinatorAndFetchResumeToken (ctx , logger , client , agentAndBuild .WorkspaceAgent .ID , "invalid" )
616
+ require .Error (t , err )
617
+ var sdkErr * codersdk.Error
618
+ require .ErrorAs (t , err , & sdkErr )
619
+ require .Equal (t , http .StatusUnauthorized , sdkErr .StatusCode ())
620
+ require .Len (t , sdkErr .Validations , 1 )
621
+ require .Equal (t , "resume_token" , sdkErr .Validations [0 ].Field )
622
+ verifiedToken = testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .verifyCalls )
623
+ require .Equal (t , "invalid" , verifiedToken )
624
+
625
+ select {
626
+ case <- resumeTokenProvider .generateCalls :
627
+ t .Fatal ("unexpected peer ID in channel" )
628
+ default :
629
+ }
573
630
})
574
- defer closer .Close ()
575
- user := coderdtest .CreateFirstUser (t , client )
576
631
577
- // Create a workspace with an agent. No need to connect it since clients can
578
- // still connect to the coordinator while the agent isn't connected.
579
- r := dbfake .WorkspaceBuild (t , api .Database , database.Workspace {
580
- OrganizationID : user .OrganizationID ,
581
- OwnerID : user .UserID ,
582
- }).WithAgent ().Do ()
583
- agentTokenUUID , err := uuid .Parse (r .AgentToken )
584
- require .NoError (t , err )
585
- ctx := testutil .Context (t , testutil .WaitLong )
586
- agentAndBuild , err := api .Database .GetWorkspaceAgentAndLatestBuildByAuthToken (dbauthz .AsSystemRestricted (ctx ), agentTokenUUID ) //nolint
587
- require .NoError (t , err )
632
+ t .Run ("BadJWT" , func (t * testing.T ) {
633
+ t .Parallel ()
588
634
589
- // Connect with no resume token, and ensure that the peer ID is set to a
590
- // random value.
591
- originalResumeToken , err := connectToCoordinatorAndFetchResumeToken (ctx , logger , client , agentAndBuild .WorkspaceAgent .ID , "" )
592
- require .NoError (t , err )
593
- originalPeerID := testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .generateCalls )
594
- require .NotEqual (t , originalPeerID , uuid .Nil )
635
+ logger := slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
636
+ clock := quartz .NewMock (t )
637
+ resumeTokenSigningKey , err := tailnet .GenerateResumeTokenSigningKey ()
638
+ mgr := cryptokeys.StaticKey {
639
+ ID : uuid .New ().String (),
640
+ Key : resumeTokenSigningKey [:],
641
+ }
642
+ require .NoError (t , err )
643
+ resumeTokenProvider := newResumeTokenRecordingProvider (
644
+ t ,
645
+ tailnet .NewResumeTokenKeyProvider (mgr , clock , time .Hour ),
646
+ )
647
+ client , closer , api := coderdtest .NewWithAPI (t , & coderdtest.Options {
648
+ Coordinator : tailnet .NewCoordinator (logger ),
649
+ CoordinatorResumeTokenProvider : resumeTokenProvider ,
650
+ })
651
+ defer closer .Close ()
652
+ user := coderdtest .CreateFirstUser (t , client )
595
653
596
- // Connect with a valid resume token, and ensure that the peer ID is set to
597
- // the stored value.
598
- clock .Advance (time .Second )
599
- newResumeToken , err := connectToCoordinatorAndFetchResumeToken (ctx , logger , client , agentAndBuild .WorkspaceAgent .ID , originalResumeToken )
600
- require .NoError (t , err )
601
- verifiedToken := testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .verifyCalls )
602
- require .Equal (t , originalResumeToken , verifiedToken )
603
- newPeerID := testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .generateCalls )
604
- require .Equal (t , originalPeerID , newPeerID )
605
- require .NotEqual (t , originalResumeToken , newResumeToken )
606
-
607
- // Connect with an invalid resume token, and ensure that the request is
608
- // rejected.
609
- clock .Advance (time .Second )
610
- _ , err = connectToCoordinatorAndFetchResumeToken (ctx , logger , client , agentAndBuild .WorkspaceAgent .ID , "invalid" )
611
- require .Error (t , err )
612
- var sdkErr * codersdk.Error
613
- require .ErrorAs (t , err , & sdkErr )
614
- require .Equal (t , http .StatusUnauthorized , sdkErr .StatusCode ())
615
- require .Len (t , sdkErr .Validations , 1 )
616
- require .Equal (t , "resume_token" , sdkErr .Validations [0 ].Field )
617
- verifiedToken = testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .verifyCalls )
618
- require .Equal (t , "invalid" , verifiedToken )
654
+ // Create a workspace with an agent. No need to connect it since clients can
655
+ // still connect to the coordinator while the agent isn't connected.
656
+ r := dbfake .WorkspaceBuild (t , api .Database , database.Workspace {
657
+ OrganizationID : user .OrganizationID ,
658
+ OwnerID : user .UserID ,
659
+ }).WithAgent ().Do ()
660
+ agentTokenUUID , err := uuid .Parse (r .AgentToken )
661
+ require .NoError (t , err )
662
+ ctx := testutil .Context (t , testutil .WaitLong )
663
+ agentAndBuild , err := api .Database .GetWorkspaceAgentAndLatestBuildByAuthToken (dbauthz .AsSystemRestricted (ctx ), agentTokenUUID ) //nolint
664
+ require .NoError (t , err )
619
665
620
- select {
621
- case <- resumeTokenProvider .generateCalls :
622
- t .Fatal ("unexpected peer ID in channel" )
623
- default :
624
- }
666
+ // Connect with no resume token, and ensure that the peer ID is set to a
667
+ // random value.
668
+ originalResumeToken , err := connectToCoordinatorAndFetchResumeToken (ctx , logger , client , agentAndBuild .WorkspaceAgent .ID , "" )
669
+ require .NoError (t , err )
670
+ originalPeerID := testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .generateCalls )
671
+ require .NotEqual (t , originalPeerID , uuid .Nil )
672
+
673
+ // Connect with an outdated token, and ensure that the peer ID is set to a
674
+ // random value. We don't want to fail requests just because
675
+ // a user got unlucky during a deployment upgrade.
676
+ outdatedToken := generateBadJWT (t , jwtutils.RegisteredClaims {
677
+ Subject : originalPeerID .String (),
678
+ Expiry : jwt .NewNumericDate (clock .Now ().Add (time .Minute )),
679
+ })
680
+
681
+ clock .Advance (time .Second )
682
+ newResumeToken , err := connectToCoordinatorAndFetchResumeToken (ctx , logger , client , agentAndBuild .WorkspaceAgent .ID , outdatedToken )
683
+ require .NoError (t , err )
684
+ verifiedToken := testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .verifyCalls )
685
+ require .Equal (t , outdatedToken , verifiedToken )
686
+ newPeerID := testutil .RequireRecvCtx (ctx , t , resumeTokenProvider .generateCalls )
687
+ require .NotEqual (t , originalPeerID , newPeerID )
688
+ require .NotEqual (t , originalResumeToken , newResumeToken )
689
+ })
625
690
}
626
691
627
692
// connectToCoordinatorAndFetchResumeToken connects to the tailnet coordinator
0 commit comments