blob: 99cd62acde4c2ca0e2a0020fc4637fc029ba99a1 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mock
import (
"encoding/json"
"fmt"
"net"
"net/http"
"reflect"
"strings"
"sync"
"testing"
"time"
)
import (
"istio.io/pkg/log"
)
var (
FakeFederatedToken = "FakeFederatedToken"
FakeAccessToken = "FakeAccessToken"
FakeTrustDomain = "FakeTrustDomain"
// FakeSubjectToken is a fake JWT token without signing, anyone can change it if needed by base64 decoding or using online tool like https://jwt.io/.
FakeSubjectToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjNZemdPQnpLcENublNPSWVPZGNyWjktMGV5UXdRc2V3RHgtOWxmS20tMWcifQ.eyJhdWQiOlsiRmFrZVRydXN0RG9tYWluIl0sImV4cCI6MTU5Njg5NjI0NiwiaWF0IjoxNTk2ODUzMDQ2LCJpc3MiOiJodHRwczovL2NvbnRhaW5lci5nb29nbGVhcGlzLmNvbS92MS9wcm9qZWN0cy90ZXN0cHJvai9sb2NhdGlvbnMvdXMtY2VudHJhbDEtYy9jbHVzdGVycy9jbHVzdGVyLTEiLCJrdWJlcm5ldGVzLmlvIjp7Im5hbWVzcGFjZSI6ImRlZmF1bHQiLCJwb2QiOnsibmFtZSI6InByb2R1Y3RwYWdlLXYxLTdmNGNjOTg4YzYtZG1kcWciLCJ1aWQiOiIzYzUwMDYwZC05OGQwLTRmNzItYTM5Zi0zZmMyODFjNjdiM2EifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImJvb2tpbmZvLXByb2R1Y3RwYWdlIiwidWlkIjoiYTlhNzE4NWUtZjhjOC00NGVlLTgzYzMtZDgyOWZjZDk4M2FiIn19LCJuYmYiOjE1OTY4NTMwNDYsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmJvb2tpbmZvLXByb2R1Y3RwYWdlIn0=.blablablafoofoofoofakesignature" // nolint: lll
FakeProjectNum = "1234567"
FakeGKEClusterURL = "https://container.googleapis.com/v1/projects/fakeproject/locations/fakelocation/clusters/fakecluster"
FakeExpiresInSeconds = 3600
)
type federatedTokenRequest struct {
Audience string `json:"audience"`
GrantType string `json:"grantType"`
RequestedTokenType string `json:"requestedTokenType"`
SubjectTokenType string `json:"subjectTokenType"`
SubjectToken string `json:"subjectToken"`
Scope string `json:"scope"`
}
type federatedTokenResponse struct {
AccessToken string `json:"access_token"`
IssuedTokenType string `json:"issued_token_type"`
TokenType string `json:"token_type"`
ExpiresIn int32 `json:"expires_in"` // Expiration time in seconds
}
type Duration struct {
// Signed seconds of the span of time. Must be from -315,576,000,000
// to +315,576,000,000 inclusive. Note: these bounds are computed from:
// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
Seconds int64 `json:"seconds"`
}
type accessTokenRequest struct {
Name string `json:"name"`
Delegates []string `json:"delegates"` // nolint: structcheck, unused
Scope []string `json:"scope"`
LifeTime Duration `json:"lifetime"` // nolint: structcheck, unused
}
type accessTokenResponse struct {
AccessToken string `json:"accessToken"`
ExpireTime string `json:"expireTime"`
}
// AuthorizationServer mocks google secure token server.
// nolint: maligned
type AuthorizationServer struct {
Port int
URL string
server *http.Server
t *testing.T
// These fields are needed to handle accessTokenRequest
expectedFederatedTokenRequest federatedTokenRequest
expectedAccessTokenRequest accessTokenRequest
mutex sync.RWMutex
generateFederatedTokenError error
generateAccessTokenError error
accessTokenLife int // life of issued access token in seconds
accessToken string
enableDynamicAccessToken bool // whether generates different token each time
numGetFederatedTokenCalls int
numGetAccessTokenCalls int
blockFederatedTokenRequest bool
blockAccessTokenRequest bool
}
type Config struct {
Port int
SubjectToken string
TrustDomain string
AccessToken string
}
// StartNewServer creates a mock server and starts it. The server listens on
// port for requests. If port is 0, a randomly chosen port is in use.
func StartNewServer(t *testing.T, conf Config) (*AuthorizationServer, error) {
st := FakeSubjectToken
if conf.SubjectToken != "" {
st = conf.SubjectToken
}
aud := fmt.Sprintf("identitynamespace:%s:%s", FakeTrustDomain, FakeGKEClusterURL)
if conf.TrustDomain != "" {
aud = fmt.Sprintf("identitynamespace:%s:%s", conf.TrustDomain, FakeGKEClusterURL)
}
token := FakeAccessToken
if conf.AccessToken != "" {
token = conf.AccessToken
}
server := &AuthorizationServer{
t: t,
expectedFederatedTokenRequest: federatedTokenRequest{
Audience: aud,
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
SubjectToken: st,
Scope: "https://www.googleapis.com/auth/cloud-platform",
},
expectedAccessTokenRequest: accessTokenRequest{
Name: fmt.Sprintf("projects/-/serviceAccounts/service-%s@gcp-sa-meshdataplane.iam.gserviceaccount.com:generateAccessToken", FakeProjectNum),
Scope: []string{"https://www.googleapis.com/auth/cloud-platform"},
},
accessTokenLife: 3600,
accessToken: token,
enableDynamicAccessToken: false,
blockFederatedTokenRequest: false,
blockAccessTokenRequest: false,
}
return server, server.Start(conf.Port)
}
func (ms *AuthorizationServer) SetGenFedTokenError(err error) {
ms.mutex.Lock()
defer ms.mutex.Unlock()
ms.generateFederatedTokenError = err
}
func (ms *AuthorizationServer) BlockFederatedTokenRequest(block bool) {
ms.mutex.Lock()
defer ms.mutex.Unlock()
ms.blockFederatedTokenRequest = block
}
func (ms *AuthorizationServer) BlockAccessTokenRequest(block bool) {
ms.mutex.Lock()
defer ms.mutex.Unlock()
ms.blockAccessTokenRequest = block
}
func (ms *AuthorizationServer) SetGenAcsTokenError(err error) {
ms.mutex.Lock()
defer ms.mutex.Unlock()
ms.generateAccessTokenError = err
}
// SetTokenLifeTime sets life time of issued access token to d seconds
func (ms *AuthorizationServer) SetTokenLifeTime(d int) {
ms.mutex.Lock()
defer ms.mutex.Unlock()
ms.accessTokenLife = d
}
// SetAccessToken sets the issued access token to token
func (ms *AuthorizationServer) SetAccessToken(token string) {
ms.mutex.Lock()
defer ms.mutex.Unlock()
ms.accessToken = token
}
// SetAccessToken sets the issued access token to token
func (ms *AuthorizationServer) EnableDynamicAccessToken(enable bool) {
ms.mutex.Lock()
defer ms.mutex.Unlock()
ms.enableDynamicAccessToken = enable
}
func (ms *AuthorizationServer) NumGetAccessTokenCalls() int {
ms.mutex.Lock()
defer ms.mutex.Unlock()
return ms.numGetAccessTokenCalls
}
func (ms *AuthorizationServer) NumGetFederatedTokenCalls() int {
ms.mutex.Lock()
defer ms.mutex.Unlock()
return ms.numGetFederatedTokenCalls
}
// Start starts the mock server.
func (ms *AuthorizationServer) Start(port int) error {
atEndpoint := fmt.Sprintf("/v1/projects/-/serviceAccounts/service-%s@gcp-sa-meshdataplane.iam.gserviceaccount.com:generateAccessToken", FakeProjectNum)
mux := http.NewServeMux()
mux.HandleFunc("/v1/token", ms.getFederatedToken)
mux.HandleFunc(atEndpoint, ms.getAccessToken)
ms.t.Logf("Registered handler for endpoints:\n%s\n%s", atEndpoint, "/v1/token")
ms.server = &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", port),
Handler: mux,
}
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
log.Errorf("Server failed to listen %v", err)
return err
}
// If passed in port is 0, get the actual chosen port.
port = ln.Addr().(*net.TCPAddr).Port
ms.Port = port
ms.URL = fmt.Sprintf("http://localhost:%d", port)
go func() {
if err := ms.server.Serve(ln); err != nil {
log.Errorf("Server failed to serve in %q: %v", ms.URL, err)
}
}()
// sleep a while for mock server to start.
time.Sleep(time.Second)
return nil
}
// Stop stops he mock server.
func (ms *AuthorizationServer) Stop() error {
if ms.server == nil {
return nil
}
return ms.server.Close()
}
func (ms *AuthorizationServer) getFederatedToken(w http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var request federatedTokenRequest
err := decoder.Decode(&request)
if err != nil {
ms.t.Errorf("invalid federatedTokenRequest: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
ms.mutex.Lock()
ms.numGetFederatedTokenCalls++
want := ms.expectedFederatedTokenRequest
fakeErr := ms.generateFederatedTokenError
blockRequest := ms.blockFederatedTokenRequest
ms.mutex.Unlock()
if blockRequest {
time.Sleep(1 * time.Hour)
}
if req.Header.Get("Content-Type") != "application/json" {
ms.t.Errorf("Content-Type header does not match\nwant %s\n got %s",
"application/json", req.Header.Get("Content-Type"))
}
if !reflect.DeepEqual(want, request) {
ms.t.Errorf("wrong federatedTokenRequest\nwant %+v\n got %+v", want, request)
w.WriteHeader(http.StatusBadRequest)
return
}
if fakeErr != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
resp := federatedTokenResponse{
AccessToken: FakeFederatedToken,
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
TokenType: "Bearer",
ExpiresIn: int32(FakeExpiresInSeconds),
}
_ = json.NewEncoder(w).Encode(resp)
}
func (ms *AuthorizationServer) getAccessToken(w http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var request accessTokenRequest
err := decoder.Decode(&request)
if err != nil {
ms.t.Errorf("invalid accessTokenRequest: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
ms.mutex.Lock()
ms.numGetAccessTokenCalls++
want := ms.expectedAccessTokenRequest
fakeErr := ms.generateAccessTokenError
tokenLife := time.Now().Add(time.Duration(ms.accessTokenLife) * time.Second)
token := ms.accessToken
if ms.enableDynamicAccessToken {
token += time.Now().String()
}
blockRequest := ms.blockAccessTokenRequest
ms.mutex.Unlock()
if blockRequest {
time.Sleep(1 * time.Hour)
}
if req.Header.Get("Authorization") != "" {
auth := req.Header.Get("Authorization")
if strings.TrimPrefix(auth, "Bearer ") != FakeFederatedToken {
ms.t.Errorf("Authorization header does not match\nwant %s\ngot %s",
FakeFederatedToken, auth)
}
} else {
ms.t.Error("missing Authorization header")
}
if req.Header.Get("Content-Type") != "application/json" {
ms.t.Errorf("Content-Type header does not match\nwant %s\n got %s",
"application/json", req.Header.Get("Content-Type"))
}
if !reflect.DeepEqual(want.Scope, request.Scope) {
ms.t.Errorf("wrong federatedTokenRequest\nwant %+v\n got %+v", want, request)
w.WriteHeader(http.StatusBadRequest)
return
}
if fakeErr != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
resp := accessTokenResponse{
AccessToken: token,
ExpireTime: tokenLife.Format(time.RFC3339Nano),
}
_ = json.NewEncoder(w).Encode(resp)
}