blob: 48a96b0c680156bae7f10854f0bb4b49da4ed85d [file] [log] [blame]
// Copyright 2015 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 registration
import (
"context"
"errors"
"fmt"
"testing"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/data/rand/cryptorand"
"go.chromium.org/luci/gae/filter/featureBreaker"
ds "go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/logdog/api/config/svcconfig"
logdog "go.chromium.org/luci/logdog/api/endpoints/coordinator/registration/v1"
"go.chromium.org/luci/logdog/appengine/coordinator"
ct "go.chromium.org/luci/logdog/appengine/coordinator/coordinatorTest"
"go.chromium.org/luci/logdog/common/types"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
"go.chromium.org/luci/server/auth/realms"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestRegisterPrefix(t *testing.T) {
t.Parallel()
Convey(`With a testing configuration`, t, func() {
c, env := ct.Install()
c, fb := featureBreaker.FilterRDS(c, nil)
// Mock random number generator so we can predict secrets.
c = cryptorand.MockForTest(c, 0)
randSecret := []byte{
250, 18, 249, 42, 251, 224, 15, 133, 8, 208, 232, 59,
171, 156, 248, 206, 191, 66, 226, 94, 139, 20, 234, 252,
129, 234, 224, 208, 15, 44, 173, 228, 193, 124, 22, 209,
}
nonce := []byte{
20, 57, 74, 77, 46, 8, 108, 144, 0, 31, 138, 106, 154, 46, 215, 125, 142,
23, 246, 192, 81, 14, 187, 119, 241, 171, 82, 85, 182, 233, 7, 119,
}
const project = "some-project"
const realm = "some-realm"
env.AddProject(c, project)
env.ActAsWriter(project, realm)
req := logdog.RegisterPrefixRequest{
Project: project,
Realm: realm,
Prefix: "testing/prefix",
SourceInfo: []string{"unit test"},
OpNonce: nonce,
}
pfx := &coordinator.LogPrefix{ID: coordinator.LogPrefixID(types.StreamName(req.Prefix))}
svr := New()
Convey(`Authorization rules`, func() {
const (
User = "user:caller@example.com"
Anon = "anonymous:anonymous"
Legacy = "user:legacy-caller@example.com"
)
const (
NoRealm = ""
AllowedRealm = "allowed"
ForbiddenRealm = "forbidden"
)
realm := func(short string) string {
return realms.Join(project, short)
}
authDB := authtest.NewFakeDB(
authtest.MockPermission(User, realm(AllowedRealm), coordinator.PermLogsCreate),
authtest.MockPermission(Legacy, realm(realms.LegacyRealm), coordinator.PermLogsCreate),
)
cases := []struct {
ident identity.Identity // who's making the call
realm string // the realm in the RPC
code codes.Code // the expected gRPC code
}{
{User, AllowedRealm, codes.OK},
{User, ForbiddenRealm, codes.PermissionDenied},
{Anon, ForbiddenRealm, codes.Unauthenticated},
// Fallback to "@legacy" realm.
{User, NoRealm, codes.PermissionDenied},
{Legacy, NoRealm, codes.OK},
{Anon, NoRealm, codes.Unauthenticated},
}
for i, test := range cases {
Convey(fmt.Sprintf("Case #%d", i), func() {
// Note: this overrides mocks set by ActAsWriter.
c := auth.WithState(c, &authtest.FakeState{
Identity: test.ident,
FakeDB: authDB,
})
req.Realm = test.realm
_, err := svr.RegisterPrefix(c, &req)
So(status.Code(err), ShouldEqual, test.code)
})
}
})
Convey(`Will register a new prefix.`, func() {
resp, err := svr.RegisterPrefix(c, &req)
So(err, ShouldBeNil)
So(resp, ShouldResemble, &logdog.RegisterPrefixResponse{
LogBundleTopic: "projects/logdog-app-id/topics/test-topic",
Secret: randSecret,
})
ct.WithProjectNamespace(c, project, func(c context.Context) {
So(ds.Get(c, pfx), ShouldBeNil)
})
So(pfx, ShouldResemble, &coordinator.LogPrefix{
Schema: coordinator.CurrentSchemaVersion,
ID: pfx.ID,
Prefix: "testing/prefix",
Realm: realms.Join(project, realm),
Created: ds.RoundTime(clock.Now(c)),
Source: []string{"unit test"},
Secret: randSecret,
OpNonce: nonce,
// 24 hours is default service prefix expiration.
Expiration: ds.RoundTime(clock.Now(c).Add(24 * time.Hour)),
})
Convey(`Will refuse to register it again without nonce.`, func() {
req.OpNonce = nil
_, err := svr.RegisterPrefix(c, &req)
So(err, ShouldBeRPCAlreadyExists)
})
Convey(`Is happy to return the same data if the nonce matches.`, func() {
resp, err := svr.RegisterPrefix(c, &req)
So(err, ShouldBeNil)
So(resp, ShouldResemble, &logdog.RegisterPrefixResponse{
LogBundleTopic: "projects/logdog-app-id/topics/test-topic",
Secret: randSecret,
})
})
Convey(`Expires the nonce after 15 minutes.`, func() {
env.Clock.Add(coordinator.RegistrationNonceTimeout + time.Second)
_, err := svr.RegisterPrefix(c, &req)
So(err, ShouldBeRPCAlreadyExists)
})
})
Convey(`Uses the correct prefix expiration`, func() {
Convey(`When service, project, and request have expiration, chooses smallest.`, func() {
env.ModProjectConfig(c, project, func(pcfg *svcconfig.ProjectConfig) {
pcfg.PrefixExpiration = durationpb.New(time.Hour)
})
req.Expiration = durationpb.New(time.Minute)
_, err := svr.RegisterPrefix(c, &req)
So(err, ShouldBeNil)
ct.WithProjectNamespace(c, project, func(c context.Context) {
So(ds.Get(c, pfx), ShouldBeNil)
})
So(pfx.Expiration, ShouldResemble, clock.Now(c).Add(time.Minute))
})
Convey(`When service, and project have expiration, chooses smallest.`, func() {
env.ModProjectConfig(c, project, func(pcfg *svcconfig.ProjectConfig) {
pcfg.PrefixExpiration = durationpb.New(time.Hour)
})
_, err := svr.RegisterPrefix(c, &req)
So(err, ShouldBeNil)
ct.WithProjectNamespace(c, project, func(c context.Context) {
So(ds.Get(c, pfx), ShouldBeNil)
})
So(pfx.Expiration, ShouldResemble, clock.Now(c).Add(time.Hour))
})
Convey(`When no expiration is defined, failed with internal error.`, func() {
env.ModServiceConfig(c, func(cfg *svcconfig.Config) {
cfg.Coordinator.PrefixExpiration = nil
})
_, err := svr.RegisterPrefix(c, &req)
So(err, ShouldBeRPCInvalidArgument, "no prefix expiration defined")
})
})
Convey(`Will fail to register the prefix if Put is broken.`, func() {
fb.BreakFeatures(errors.New("test error"), "PutMulti")
_, err := svr.RegisterPrefix(c, &req)
So(err, ShouldBeRPCInternal)
})
})
}