github.com/google/osv-scalibr@v0.4.1/veles/secrets/gcshmackey/validator_test.go (about)

     1  // Copyright 2025 Google LLC
     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 gcshmackey_test
    16  
    17  import (
    18  	"io"
    19  	"net/http"
    20  	"net/http/httptest"
    21  	"net/url"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/google/osv-scalibr/veles"
    26  	"github.com/google/osv-scalibr/veles/secrets/gcshmackey"
    27  )
    28  
    29  type fakeSigner struct{}
    30  
    31  func (n fakeSigner) Sign(r *http.Request, accessID string, secret string) error {
    32  	r.Header.Set("Authorization", "Signature="+accessID+":"+secret)
    33  	return nil
    34  }
    35  
    36  type mockRoundTripper struct {
    37  	url string
    38  }
    39  
    40  func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
    41  	if req.URL.Host == "storage.googleapis.com" {
    42  		testURL, _ := url.Parse(m.url)
    43  		req.URL.Scheme = testURL.Scheme
    44  		req.URL.Host = testURL.Host
    45  	}
    46  	return http.DefaultTransport.RoundTrip(req)
    47  }
    48  
    49  // mockS3Server returns an httptest.Server that simulates an S3 ListBuckets endpoint.
    50  // It accepts only the "testsecret" key; any other secret yields SignatureDoesNotMatch.
    51  func mockS3Server(signature string, denied bool) func() *httptest.Server {
    52  	return func() *httptest.Server {
    53  		handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    54  			// Only handle ListBuckets (GET /)
    55  			if req.Method != http.MethodGet || req.URL.Path != "/" {
    56  				http.Error(w, "not found", http.StatusNotFound)
    57  				return
    58  			}
    59  
    60  			if !strings.Contains(req.Header.Get("Authorization"), signature) {
    61  				w.WriteHeader(http.StatusForbidden)
    62  				_, _ = io.WriteString(w, `<?xml version='1.0' encoding='UTF-8'?>
    63  					<Error>
    64  						<Code>SignatureDoesNotMatch</Code>
    65  						<Message>Access denied.</Message>
    66  						<Details>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Details>
    67  						<StringToSign>**REDACTED***</StringToSign>
    68  						<CanonicalRequest>**REDACTED***</CanonicalRequest>
    69  					</Error>`)
    70  				return
    71  			}
    72  
    73  			if denied {
    74  				w.WriteHeader(http.StatusForbidden)
    75  				_, _ = io.WriteString(w, `<?xml version='1.0' encoding='UTF-8'?>
    76  					<Error>
    77  						<Code>AccessDenied</Code>
    78  						<Message>Access denied.</Message>
    79  						<Details>**SERVICE-ACCOUNT-EMAIL-REDACTED** does not have storage.buckets.list access to the Google Cloud project. Permission 'storage.buckets.list' denied on resource (or it may not exist).</Details>
    80  					</Error>`)
    81  				return
    82  			}
    83  
    84  			_, _ = io.WriteString(w, `<?xml version='1.0' encoding='UTF-8'?>
    85  				<ListAllMyBucketsResult xmlns='http://doc.s3.amazonaws.com/2006-03-01'>
    86  					<Buckets/>
    87  				</ListAllMyBucketsResult>`)
    88  			w.WriteHeader(http.StatusOK)
    89  		})
    90  
    91  		return httptest.NewServer(handler)
    92  	}
    93  }
    94  
    95  const (
    96  	exampleAccessID  = "GOOGerkjf4f034"
    97  	correctSecret    = "testsecret"
    98  	badSecret        = "badSecret"
    99  	correctSignature = exampleAccessID + ":" + correctSecret
   100  )
   101  
   102  func TestValidator(t *testing.T) {
   103  	cases := []struct {
   104  		name   string
   105  		key    gcshmackey.HMACKey
   106  		want   veles.ValidationStatus
   107  		server func() *httptest.Server
   108  	}{
   109  		{
   110  			name: "correct_secret",
   111  			key: gcshmackey.HMACKey{
   112  				AccessID: exampleAccessID,
   113  				Secret:   correctSecret,
   114  			},
   115  			want:   veles.ValidationValid,
   116  			server: mockS3Server(correctSignature, false),
   117  		},
   118  		{
   119  			name: "correct_secret,_access_denied",
   120  			key: gcshmackey.HMACKey{
   121  				AccessID: exampleAccessID,
   122  				Secret:   correctSecret,
   123  			},
   124  			want:   veles.ValidationValid,
   125  			server: mockS3Server(correctSignature, true),
   126  		},
   127  		{
   128  			name: "bad_secret",
   129  			key: gcshmackey.HMACKey{
   130  				AccessID: exampleAccessID,
   131  				Secret:   badSecret,
   132  			},
   133  			want:   veles.ValidationInvalid,
   134  			server: mockS3Server(correctSignature, true),
   135  		},
   136  	}
   137  
   138  	for _, tc := range cases {
   139  		t.Run(tc.name, func(t *testing.T) {
   140  			srv := tc.server()
   141  			client := &http.Client{
   142  				Transport: &mockRoundTripper{url: srv.URL},
   143  			}
   144  
   145  			validator := gcshmackey.NewValidator()
   146  			validator.SetHTTPClient(client)
   147  			validator.SetSigner(fakeSigner{})
   148  
   149  			got, err := validator.Validate(t.Context(), tc.key)
   150  			if err != nil {
   151  				t.Errorf("Validate() error: %v, want nil", err)
   152  			}
   153  			if got != tc.want {
   154  				t.Errorf("Validate() = %q, want %q", got, tc.want)
   155  			}
   156  		})
   157  	}
   158  }