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

     1  // Copyright 2019 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 dump
    16  
    17  import (
    18  	"context"
    19  	"crypto/sha512"
    20  	"fmt"
    21  	"net/http"
    22  	"net/http/httptest"
    23  	"testing"
    24  
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	"go.chromium.org/luci/common/retry"
    28  
    29  	"go.chromium.org/luci/server/auth/authdb"
    30  	"go.chromium.org/luci/server/auth/authtest"
    31  	"go.chromium.org/luci/server/auth/service/protocol"
    32  	"go.chromium.org/luci/server/auth/signing/signingtest"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  	. "go.chromium.org/luci/common/testing/assertions"
    36  )
    37  
    38  func TestFetcher(t *testing.T) {
    39  	t.Parallel()
    40  
    41  	signer := signingtest.NewSigner(nil)
    42  
    43  	Convey("With mocks", t, func(c C) {
    44  		serverAllowAccess := true           // is caller allowed to access authdb?
    45  		serverHasAccessNow := false         // does caller have access right now?
    46  		serverDumpPath := "bucket/prefix"   // where server dumps AuthDB
    47  		serverSignerID := "auth-service-id" // ID used to sign blob by the server
    48  		serverSignature := []byte(nil)      // if non-nil, mocked signature
    49  
    50  		serverLatestRev := int64(1234) // revision of the latest dump
    51  		serverLatestVal := "latest"    // payload in the latest dump
    52  
    53  		ctx := authtest.MockAuthConfig(context.Background())
    54  
    55  		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    56  			switch r.URL.Path {
    57  			case "/auth_service/api/v1/authdb/subscription/authorization":
    58  				if !serverAllowAccess {
    59  					w.WriteHeader(403)
    60  				} else {
    61  					serverHasAccessNow = true
    62  					w.Write([]byte(fmt.Sprintf(`{"gs":{"auth_db_gs_path":%q}}`, serverDumpPath)))
    63  				}
    64  			case "/bucket/prefix/latest.json":
    65  				if !serverHasAccessNow {
    66  					w.WriteHeader(403)
    67  				} else {
    68  					w.Write([]byte(fmt.Sprintf(`{"auth_db_rev": "%d"}`, serverLatestRev)))
    69  				}
    70  			case "/bucket/prefix/latest.db":
    71  				if !serverHasAccessNow {
    72  					w.WriteHeader(403)
    73  				} else {
    74  					w.Write(genSignedAuthDB(ctx, signer,
    75  						serverSignerID, serverSignature, serverLatestRev, serverLatestVal))
    76  				}
    77  			default:
    78  				c.So(r.URL, ShouldEqual, "")
    79  			}
    80  		}))
    81  		defer ts.Close()
    82  
    83  		signingCerts, err := signer.Certificates(ctx)
    84  		So(err, ShouldBeNil)
    85  
    86  		f := Fetcher{
    87  			StorageDumpPath:    "bucket/prefix",
    88  			AuthServiceURL:     ts.URL,
    89  			AuthServiceAccount: serverSignerID,
    90  			OAuthScopes:        []string{"scope1", "scope2"},
    91  
    92  			testRetryPolicy:   func() retry.Iterator { return &retry.Limited{Retries: 0} },
    93  			testStorageURL:    ts.URL,
    94  			testStorageClient: http.DefaultClient,
    95  			testSigningCerts:  signingCerts,
    96  		}
    97  
    98  		Convey("Fetching works", func() {
    99  			db, err := f.FetchAuthDB(ctx, nil)
   100  			So(err, ShouldBeNil)
   101  			assertAuthDB(db, serverLatestRev, serverLatestVal)
   102  
   103  			Convey("Skips updating if nothing has changed", func() {
   104  				fetched, err := f.FetchAuthDB(ctx, db)
   105  				So(err, ShouldBeNil)
   106  				So(fetched == db, ShouldBeTrue) // the exact same object
   107  			})
   108  
   109  			Convey("Updates if the revision goes up", func() {
   110  				serverLatestRev++
   111  				serverLatestVal = "newer"
   112  
   113  				fetched, err := f.FetchAuthDB(ctx, db)
   114  				So(err, ShouldBeNil)
   115  				assertAuthDB(fetched, serverLatestRev, serverLatestVal)
   116  			})
   117  
   118  			Convey("Refuses to update if the revision goes down", func() {
   119  				serverLatestRev--
   120  				serverLatestVal = "older"
   121  
   122  				fetched, err := f.FetchAuthDB(ctx, db)
   123  				So(err, ShouldBeNil)
   124  				So(fetched == db, ShouldBeTrue) // the exact same object
   125  			})
   126  		})
   127  
   128  		Convey("Not authorized", func() {
   129  			serverAllowAccess = false
   130  			_, err := f.FetchAuthDB(ctx, nil)
   131  			So(err, ShouldErrLike, "HTTP code (403)")
   132  		})
   133  
   134  		Convey("Wrong storage path", func() {
   135  			serverDumpPath = "something/else"
   136  			_, err := f.FetchAuthDB(ctx, nil)
   137  			So(err, ShouldErrLike, "wrong configuration")
   138  		})
   139  
   140  		Convey("Unexpected signer", func() {
   141  			serverSignerID = "someone-else"
   142  			_, err := f.FetchAuthDB(ctx, nil)
   143  			So(err, ShouldErrLike, "the snapshot is signed by")
   144  		})
   145  
   146  		Convey("Bad signature", func() {
   147  			serverSignature = []byte("bad signature")
   148  			_, err := f.FetchAuthDB(ctx, nil)
   149  			So(err, ShouldErrLike, "failed to verify that AuthDB was signed")
   150  		})
   151  	})
   152  }
   153  
   154  // genSignedAuthDB generates and signs auth DB blob.
   155  func genSignedAuthDB(ctx context.Context, signer *signingtest.Signer, signerID string, sig []byte, rev int64, data string) []byte {
   156  	authDB, err := proto.Marshal(&protocol.ReplicationPushRequest{
   157  		Revision: &protocol.AuthDBRevision{AuthDbRev: rev},
   158  		// We abuse TokenServerUrl to pass some payload checked by assertAuthDB.
   159  		AuthDb: &protocol.AuthDB{TokenServerUrl: data},
   160  	})
   161  	if err != nil {
   162  		panic(err)
   163  	}
   164  
   165  	hash := sha512.Sum512(authDB)
   166  	keyID, realSig, err := signer.SignBytes(ctx, hash[:])
   167  	if err != nil {
   168  		panic(err)
   169  	}
   170  	if sig == nil {
   171  		sig = realSig
   172  	}
   173  
   174  	blob, err := proto.Marshal(&protocol.SignedAuthDB{
   175  		AuthDbBlob:   authDB,
   176  		SignerId:     signerID,
   177  		SigningKeyId: keyID,
   178  		Signature:    sig,
   179  	})
   180  	if err != nil {
   181  		panic(err)
   182  	}
   183  	return blob
   184  }
   185  
   186  // assertAuthDB verifies 'db' was constructed from output of genSignedAuthDB.
   187  func assertAuthDB(db *authdb.SnapshotDB, rev int64, data string) {
   188  	So(db.Rev, ShouldEqual, rev)
   189  	d, _ := db.GetTokenServiceURL(nil)
   190  	So(d, ShouldEqual, data)
   191  }