go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/actor_access_tokens_test.go (about)

     1  // Copyright 2017 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package auth
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/http"
    21  	"net/url"
    22  	"testing"
    23  	"time"
    24  
    25  	"go.chromium.org/luci/common/clock"
    26  	"go.chromium.org/luci/common/clock/testclock"
    27  
    28  	"go.chromium.org/luci/server/caching"
    29  
    30  	. "github.com/smartystreets/goconvey/convey"
    31  )
    32  
    33  func TestMintAccessTokenForServiceAccount(t *testing.T) {
    34  	t.Parallel()
    35  
    36  	Convey("MintAccessTokenForServiceAccount works", t, func() {
    37  		ctx := context.Background()
    38  		ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
    39  		ctx = caching.WithEmptyProcessCache(ctx)
    40  
    41  		returnedToken := "token1"
    42  		lastRequest := ""
    43  
    44  		generateTokenURL := fmt.Sprintf("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken?alt=json",
    45  			url.QueryEscape("abc@example.com"))
    46  
    47  		transport := &clientRPCTransportMock{
    48  			cb: func(r *http.Request, body string) string {
    49  				lastRequest = body
    50  
    51  				expireTime, err := time.Parse(time.RFC3339, clock.Now(ctx).Add(time.Hour).UTC().Format(time.RFC3339))
    52  				if err != nil {
    53  					t.Fatalf("Unable to parse/format time: %v", err)
    54  				}
    55  
    56  				if r.URL.String() == generateTokenURL {
    57  					return fmt.Sprintf(`{"accessToken":"%s","expireTime":"%s"}`, returnedToken, expireTime.Format(time.RFC3339))
    58  				}
    59  				t.Fatalf("Unexpected request to %s", r.URL.String())
    60  				return "unknown URL"
    61  			},
    62  		}
    63  
    64  		ctx = ModifyConfig(ctx, func(cfg Config) Config {
    65  			cfg.AccessTokenProvider = transport.getAccessToken
    66  			cfg.AnonymousTransport = transport.getTransport
    67  			return cfg
    68  		})
    69  
    70  		tok, err := MintAccessTokenForServiceAccount(ctx, MintAccessTokenParams{
    71  			ServiceAccount: "abc@example.com",
    72  			Scopes:         []string{"scope_b", "scope_a"},
    73  		})
    74  		So(err, ShouldBeNil)
    75  
    76  		expectedExpireTime, err := time.Parse(time.RFC3339, clock.Now(ctx).Add(time.Hour).UTC().Format(time.RFC3339))
    77  		So(err, ShouldBeNil)
    78  
    79  		So(tok, ShouldResemble, &Token{
    80  			Token:  "token1",
    81  			Expiry: expectedExpireTime,
    82  		})
    83  
    84  		// Cached now.
    85  		So(actorAccessTokenCache.lc.CachedLocally(ctx), ShouldEqual, 1)
    86  
    87  		// On subsequence request the cached token is used.
    88  		returnedToken = "token2"
    89  		tok, err = MintAccessTokenForServiceAccount(ctx, MintAccessTokenParams{
    90  			ServiceAccount: "abc@example.com",
    91  			Scopes:         []string{"scope_b", "scope_a"},
    92  		})
    93  		So(err, ShouldBeNil)
    94  		So(tok.Token, ShouldEqual, "token1") // old one
    95  
    96  		// Unless it expires sooner than requested TTL.
    97  		clock.Get(ctx).(testclock.TestClock).Add(40 * time.Minute)
    98  		tok, err = MintAccessTokenForServiceAccount(ctx, MintAccessTokenParams{
    99  			ServiceAccount: "abc@example.com",
   100  			Scopes:         []string{"scope_b", "scope_a"},
   101  			MinTTL:         30 * time.Minute,
   102  		})
   103  		So(err, ShouldBeNil)
   104  		So(tok.Token, ShouldResemble, "token2") // new one
   105  
   106  		// Using delegates results in a different cache key.
   107  		returnedToken = "token3"
   108  		tok, err = MintAccessTokenForServiceAccount(ctx, MintAccessTokenParams{
   109  			ServiceAccount: "abc@example.com",
   110  			Scopes:         []string{"scope_b", "scope_a"},
   111  			Delegates:      []string{"d2@example.com", "d1@example.com"},
   112  			MinTTL:         30 * time.Minute,
   113  		})
   114  		So(err, ShouldBeNil)
   115  		So(tok.Token, ShouldResemble, "token3") // new one
   116  		So(lastRequest, ShouldEqual,
   117  			`{"delegates":["projects/-/serviceAccounts/d2@example.com","projects/-/serviceAccounts/d1@example.com"],"scope":["scope_a","scope_b"]}`)
   118  	})
   119  }