blob: b46d7fab7d32b70167bd44d1f831f93159e7da4a [file] [log] [blame]
// Copyright 2019 The LUCI 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 dump
import (
"context"
"crypto/sha512"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"google.golang.org/protobuf/proto"
"go.chromium.org/luci/common/retry"
"go.chromium.org/luci/server/auth/authdb"
"go.chromium.org/luci/server/auth/authtest"
"go.chromium.org/luci/server/auth/service/protocol"
"go.chromium.org/luci/server/auth/signing/signingtest"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestFetcher(t *testing.T) {
t.Parallel()
signer := signingtest.NewSigner(nil)
Convey("With mocks", t, func(c C) {
serverAllowAccess := true // is caller allowed to access authdb?
serverHasAccessNow := false // does caller have access right now?
serverDumpPath := "bucket/prefix" // where server dumps AuthDB
serverSignerID := "auth-service-id" // ID used to sign blob by the server
serverSignature := []byte(nil) // if non-nil, mocked signature
serverLatestRev := int64(1234) // revision of the latest dump
serverLatestVal := "latest" // payload in the latest dump
ctx := authtest.MockAuthConfig(context.Background())
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/auth_service/api/v1/authdb/subscription/authorization":
if !serverAllowAccess {
w.WriteHeader(403)
} else {
serverHasAccessNow = true
w.Write([]byte(fmt.Sprintf(`{"gs":{"auth_db_gs_path":%q}}`, serverDumpPath)))
}
case "/bucket/prefix/latest.json":
if !serverHasAccessNow {
w.WriteHeader(403)
} else {
w.Write([]byte(fmt.Sprintf(`{"auth_db_rev": "%d"}`, serverLatestRev)))
}
case "/bucket/prefix/latest.db":
if !serverHasAccessNow {
w.WriteHeader(403)
} else {
w.Write(genSignedAuthDB(ctx, signer,
serverSignerID, serverSignature, serverLatestRev, serverLatestVal))
}
default:
c.So(r.URL, ShouldEqual, "")
}
}))
defer ts.Close()
signingCerts, err := signer.Certificates(ctx)
So(err, ShouldBeNil)
f := Fetcher{
StorageDumpPath: "bucket/prefix",
AuthServiceURL: ts.URL,
AuthServiceAccount: serverSignerID,
OAuthScopes: []string{"scope1", "scope2"},
testRetryPolicy: func() retry.Iterator { return &retry.Limited{Retries: 0} },
testStorageURL: ts.URL,
testStorageClient: http.DefaultClient,
testSigningCerts: signingCerts,
}
Convey("Fetching works", func() {
db, err := f.FetchAuthDB(ctx, nil)
So(err, ShouldBeNil)
assertAuthDB(db, serverLatestRev, serverLatestVal)
Convey("Skips updating if nothing has changed", func() {
fetched, err := f.FetchAuthDB(ctx, db)
So(err, ShouldBeNil)
So(fetched == db, ShouldBeTrue) // the exact same object
})
Convey("Updates if the revision goes up", func() {
serverLatestRev++
serverLatestVal = "newer"
fetched, err := f.FetchAuthDB(ctx, db)
So(err, ShouldBeNil)
assertAuthDB(fetched, serverLatestRev, serverLatestVal)
})
Convey("Refuses to update if the revision goes down", func() {
serverLatestRev--
serverLatestVal = "older"
fetched, err := f.FetchAuthDB(ctx, db)
So(err, ShouldBeNil)
So(fetched == db, ShouldBeTrue) // the exact same object
})
})
Convey("Not authorized", func() {
serverAllowAccess = false
_, err := f.FetchAuthDB(ctx, nil)
So(err, ShouldErrLike, "HTTP code (403)")
})
Convey("Wrong storage path", func() {
serverDumpPath = "something/else"
_, err := f.FetchAuthDB(ctx, nil)
So(err, ShouldErrLike, "wrong configuration")
})
Convey("Unexpected signer", func() {
serverSignerID = "someone-else"
_, err := f.FetchAuthDB(ctx, nil)
So(err, ShouldErrLike, "the snapshot is signed by")
})
Convey("Bad signature", func() {
serverSignature = []byte("bad signature")
_, err := f.FetchAuthDB(ctx, nil)
So(err, ShouldErrLike, "failed to verify that AuthDB was signed")
})
})
}
// genSignedAuthDB generates and signs auth DB blob.
func genSignedAuthDB(ctx context.Context, signer *signingtest.Signer, signerID string, sig []byte, rev int64, data string) []byte {
authDB, err := proto.Marshal(&protocol.ReplicationPushRequest{
Revision: &protocol.AuthDBRevision{AuthDbRev: rev},
// We abuse TokenServerUrl to pass some payload checked by assertAuthDB.
AuthDb: &protocol.AuthDB{TokenServerUrl: data},
})
if err != nil {
panic(err)
}
hash := sha512.Sum512(authDB)
keyID, realSig, err := signer.SignBytes(ctx, hash[:])
if err != nil {
panic(err)
}
if sig == nil {
sig = realSig
}
blob, err := proto.Marshal(&protocol.SignedAuthDB{
AuthDbBlob: authDB,
SignerId: signerID,
SigningKeyId: keyID,
Signature: sig,
})
if err != nil {
panic(err)
}
return blob
}
// assertAuthDB verifies 'db' was constructed from output of genSignedAuthDB.
func assertAuthDB(db *authdb.SnapshotDB, rev int64, data string) {
So(db.Rev, ShouldEqual, rev)
d, _ := db.GetTokenServiceURL(nil)
So(d, ShouldEqual, data)
}