go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/botsrv/botsrv_test.go (about)

     1  // Copyright 2022 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 botsrv
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"io"
    22  	"net"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"testing"
    26  	"time"
    27  
    28  	"google.golang.org/protobuf/proto"
    29  	"google.golang.org/protobuf/types/known/timestamppb"
    30  
    31  	"go.chromium.org/luci/auth/identity"
    32  	"go.chromium.org/luci/common/clock/testclock"
    33  	"go.chromium.org/luci/server/auth"
    34  	"go.chromium.org/luci/server/auth/authtest"
    35  	"go.chromium.org/luci/server/auth/openid"
    36  	"go.chromium.org/luci/server/router"
    37  	"go.chromium.org/luci/server/secrets"
    38  	"go.chromium.org/luci/tokenserver/auth/machine"
    39  
    40  	internalspb "go.chromium.org/luci/swarming/proto/internals"
    41  	"go.chromium.org/luci/swarming/server/hmactoken"
    42  
    43  	. "github.com/smartystreets/goconvey/convey"
    44  	. "go.chromium.org/luci/common/testing/assertions"
    45  )
    46  
    47  type testRequest struct {
    48  	Dimensions   map[string][]string
    49  	PollToken    []byte
    50  	SessionToken []byte
    51  }
    52  
    53  func (r *testRequest) ExtractPollToken() []byte               { return r.PollToken }
    54  func (r *testRequest) ExtractSessionToken() []byte            { return r.SessionToken }
    55  func (r *testRequest) ExtractDimensions() map[string][]string { return r.Dimensions }
    56  func (r *testRequest) ExtractDebugRequest() any               { return r }
    57  
    58  func TestBotHandler(t *testing.T) {
    59  	t.Parallel()
    60  
    61  	Convey("With server", t, func() {
    62  		now := time.Date(2044, time.April, 4, 4, 4, 4, 4, time.UTC)
    63  		ctx := context.Background()
    64  		ctx, _ = testclock.UseTime(ctx, now)
    65  
    66  		ctx = auth.WithState(ctx, &authtest.FakeState{
    67  			Identity: "bot:ignored",
    68  			UserExtra: &machine.MachineTokenInfo{
    69  				FQDN: "bot.fqdn",
    70  			},
    71  		})
    72  
    73  		srv := &Server{
    74  			router: router.New(),
    75  			hmacSecret: hmactoken.NewStaticSecret(secrets.Secret{
    76  				Active:  []byte("secret"),
    77  				Passive: [][]byte{[]byte("also-secret")},
    78  			}),
    79  		}
    80  
    81  		var lastBody *testRequest
    82  		var lastRequest *Request
    83  		var nextResponse Response
    84  		var nextError error
    85  		InstallHandler(srv, "/test", func(_ context.Context, body *testRequest, r *Request) (Response, error) {
    86  			lastBody = body
    87  			lastRequest = r
    88  			return nextResponse, nextError
    89  		})
    90  
    91  		callRaw := func(body []byte, ct string, mockedResp Response, mockedErr error) (b *testRequest, req *Request, status int, resp string) {
    92  			lastRequest = nil
    93  			nextResponse = mockedResp
    94  			nextError = mockedErr
    95  			rq := httptest.NewRequest("POST", "/test", bytes.NewReader(body)).WithContext(ctx)
    96  			if ct != "" {
    97  				rq.Header.Set("Content-Type", ct)
    98  			}
    99  			rw := httptest.NewRecorder()
   100  			srv.router.ServeHTTP(rw, rq)
   101  			res := rw.Result()
   102  			if res.StatusCode == http.StatusOK {
   103  				So(res.Header.Get("Content-Type"), ShouldEqual, "application/json; charset=utf-8")
   104  			}
   105  			respBody, _ := io.ReadAll(res.Body)
   106  			return lastBody, lastRequest, res.StatusCode, string(respBody)
   107  		}
   108  
   109  		call := func(body testRequest, mockedResp Response, mockedErr error) (b *testRequest, req *Request, status int, resp string) {
   110  			blob, err := json.Marshal(&body)
   111  			So(err, ShouldBeNil)
   112  			return callRaw(blob, "application/json; charset=utf-8", mockedResp, mockedErr)
   113  		}
   114  
   115  		makePollState := func(id string) *internalspb.PollState {
   116  			return &internalspb.PollState{
   117  				Id:          id,
   118  				Expiry:      timestamppb.New(now.Add(5 * time.Minute)),
   119  				RbeInstance: "some-rbe-instance",
   120  				EnforcedDimensions: []*internalspb.PollState_Dimension{
   121  					{Key: "id", Values: []string{"bot-id"}},
   122  				},
   123  				AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{
   124  					LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{
   125  						MachineFqdn: "bot.fqdn",
   126  					},
   127  				},
   128  			}
   129  		}
   130  
   131  		Convey("Happy path with poll token", func() {
   132  			pollState := makePollState("poll-state-id")
   133  
   134  			req := testRequest{
   135  				Dimensions: map[string][]string{
   136  					"id":   {"bot-id"},
   137  					"pool": {"pool"},
   138  				},
   139  				PollToken: genToken(pollState, []byte("also-secret")),
   140  			}
   141  
   142  			body, seenReq, status, resp := call(req, "some-response", nil)
   143  			So(status, ShouldEqual, http.StatusOK)
   144  			So(resp, ShouldEqual, "\"some-response\"\n")
   145  			So(body, ShouldResemble, &req)
   146  			So(seenReq.BotID, ShouldEqual, "bot-id")
   147  			So(seenReq.SessionID, ShouldEqual, "")
   148  			So(seenReq.SessionTokenExpired, ShouldBeFalse)
   149  			So(seenReq.PollState, ShouldResembleProto, pollState)
   150  		})
   151  
   152  		Convey("Happy path with session token", func() {
   153  			pollState := makePollState("poll-state-id")
   154  
   155  			req := testRequest{
   156  				Dimensions: map[string][]string{
   157  					"id":   {"bot-id"},
   158  					"pool": {"pool"},
   159  				},
   160  				SessionToken: genToken(&internalspb.BotSession{
   161  					RbeBotSessionId: "bot-session-id",
   162  					PollState:       pollState,
   163  					Expiry:          timestamppb.New(now.Add(5 * time.Minute)),
   164  				}, []byte("also-secret")),
   165  			}
   166  
   167  			body, seenReq, status, resp := call(req, "some-response", nil)
   168  			So(status, ShouldEqual, http.StatusOK)
   169  			So(resp, ShouldEqual, "\"some-response\"\n")
   170  			So(body, ShouldResemble, &req)
   171  			So(seenReq.BotID, ShouldEqual, "bot-id")
   172  			So(seenReq.SessionID, ShouldEqual, "bot-session-id")
   173  			So(seenReq.SessionTokenExpired, ShouldBeFalse)
   174  			So(seenReq.PollState, ShouldResembleProto, pollState)
   175  		})
   176  
   177  		Convey("Happy path with both tokens", func() {
   178  			pollStateInPollToken := makePollState("in-poll-token")
   179  			pollStateInSessionToken := makePollState("in-session-token")
   180  
   181  			req := testRequest{
   182  				Dimensions: map[string][]string{
   183  					"id":   {"bot-id"},
   184  					"pool": {"pool"},
   185  				},
   186  				PollToken: genToken(pollStateInPollToken, []byte("also-secret")),
   187  				SessionToken: genToken(&internalspb.BotSession{
   188  					RbeBotSessionId: "bot-session-id",
   189  					PollState:       pollStateInSessionToken,
   190  					Expiry:          timestamppb.New(now.Add(5 * time.Minute)),
   191  				}, []byte("also-secret")),
   192  			}
   193  
   194  			body, seenReq, status, resp := call(req, "some-response", nil)
   195  			So(status, ShouldEqual, http.StatusOK)
   196  			So(resp, ShouldEqual, "\"some-response\"\n")
   197  			So(body, ShouldResemble, &req)
   198  			So(seenReq.BotID, ShouldEqual, "bot-id")
   199  			So(seenReq.SessionID, ShouldEqual, "bot-session-id")
   200  			So(seenReq.SessionTokenExpired, ShouldBeFalse)
   201  			So(seenReq.PollState, ShouldResembleProto, pollStateInPollToken)
   202  		})
   203  
   204  		Convey("Happy path with session token and expired poll token", func() {
   205  			pollStateInPollToken := makePollState("in-poll-token")
   206  			pollStateInPollToken.Expiry = timestamppb.New(now.Add(-5 * time.Minute))
   207  
   208  			pollStateInSessionToken := makePollState("in-session-token")
   209  
   210  			req := testRequest{
   211  				Dimensions: map[string][]string{
   212  					"id":   {"bot-id"},
   213  					"pool": {"pool"},
   214  				},
   215  				PollToken: genToken(pollStateInPollToken, []byte("also-secret")),
   216  				SessionToken: genToken(&internalspb.BotSession{
   217  					RbeBotSessionId: "bot-session-id",
   218  					PollState:       pollStateInSessionToken,
   219  					Expiry:          timestamppb.New(now.Add(5 * time.Minute)),
   220  				}, []byte("also-secret")),
   221  			}
   222  
   223  			body, seenReq, status, resp := call(req, "some-response", nil)
   224  			So(status, ShouldEqual, http.StatusOK)
   225  			So(resp, ShouldEqual, "\"some-response\"\n")
   226  			So(body, ShouldResemble, &req)
   227  			So(seenReq.BotID, ShouldEqual, "bot-id")
   228  			So(seenReq.SessionID, ShouldEqual, "bot-session-id")
   229  			So(seenReq.SessionTokenExpired, ShouldBeFalse)
   230  
   231  			// Used the session token.
   232  			So(seenReq.PollState, ShouldResembleProto, pollStateInSessionToken)
   233  		})
   234  
   235  		Convey("Happy path with poll token and expired session token", func() {
   236  			pollStateInPollToken := makePollState("in-poll-token")
   237  
   238  			pollStateInSessionToken := makePollState("in-session-token")
   239  			pollStateInSessionToken.Expiry = timestamppb.New(now.Add(-5 * time.Minute))
   240  
   241  			req := testRequest{
   242  				Dimensions: map[string][]string{
   243  					"id":   {"bot-id"},
   244  					"pool": {"pool"},
   245  				},
   246  				PollToken: genToken(pollStateInPollToken, []byte("also-secret")),
   247  				SessionToken: genToken(&internalspb.BotSession{
   248  					RbeBotSessionId: "bot-session-id",
   249  					PollState:       pollStateInSessionToken,
   250  					Expiry:          pollStateInSessionToken.Expiry,
   251  				}, []byte("also-secret")),
   252  			}
   253  
   254  			body, seenReq, status, resp := call(req, "some-response", nil)
   255  			So(status, ShouldEqual, http.StatusOK)
   256  			So(resp, ShouldEqual, "\"some-response\"\n")
   257  			So(body, ShouldResemble, &req)
   258  			So(seenReq.BotID, ShouldEqual, "bot-id")
   259  			So(seenReq.SessionID, ShouldEqual, "")
   260  			So(seenReq.SessionTokenExpired, ShouldBeTrue)
   261  
   262  			// Used the poll token.
   263  			So(seenReq.PollState, ShouldResembleProto, pollStateInPollToken)
   264  		})
   265  
   266  		Convey("Wrong bot credentials", func() {
   267  			pollState := &internalspb.PollState{
   268  				Id:          "poll-state-id",
   269  				Expiry:      timestamppb.New(now.Add(5 * time.Minute)),
   270  				RbeInstance: "some-rbe-instance",
   271  				EnforcedDimensions: []*internalspb.PollState_Dimension{
   272  					{Key: "id", Values: []string{"bot-id"}},
   273  				},
   274  				AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{
   275  					LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{
   276  						MachineFqdn: "another.fqdn",
   277  					},
   278  				},
   279  			}
   280  
   281  			req := testRequest{
   282  				Dimensions: map[string][]string{
   283  					"id": {"bot-id"},
   284  				},
   285  				PollToken: genToken(pollState, []byte("also-secret")),
   286  			}
   287  
   288  			_, seenReq, status, resp := call(req, "some-response", nil)
   289  			So(seenReq, ShouldBeNil)
   290  			So(status, ShouldEqual, http.StatusUnauthorized)
   291  			So(resp, ShouldContainSubstring, "bad bot credentials: wrong FQDN in the LUCI machine token")
   292  		})
   293  
   294  		Convey("Bad Content-Type", func() {
   295  			_, seenReq, status, resp := callRaw([]byte("ignored"), "application/x-www-form-urlencoded", nil, nil)
   296  			So(seenReq, ShouldBeNil)
   297  			So(status, ShouldEqual, http.StatusBadRequest)
   298  			So(resp, ShouldContainSubstring, "bad content type")
   299  		})
   300  
   301  		Convey("Not JSON", func() {
   302  			_, seenReq, status, resp := callRaw([]byte("what is this"), "application/json; charset=utf-8", nil, nil)
   303  			So(seenReq, ShouldBeNil)
   304  			So(status, ShouldEqual, http.StatusBadRequest)
   305  			So(resp, ShouldContainSubstring, "failed to deserialized")
   306  		})
   307  
   308  		Convey("Wrong poll token", func() {
   309  			req := testRequest{
   310  				PollToken: genToken(&internalspb.BotSession{
   311  					RbeBotSessionId: "not-a-poll-token",
   312  					Expiry:          timestamppb.New(now.Add(5 * time.Minute)),
   313  				}, []byte("also-secret")),
   314  			}
   315  			_, seenReq, status, resp := call(req, "some-response", nil)
   316  			So(seenReq, ShouldBeNil)
   317  			So(status, ShouldEqual, http.StatusUnauthorized)
   318  			So(resp, ShouldContainSubstring, "failed to verify poll token: invalid payload type")
   319  		})
   320  
   321  		Convey("Wrong session token", func() {
   322  			req := testRequest{
   323  				SessionToken: genToken(&internalspb.PollState{
   324  					Id:     "not-a-session-token",
   325  					Expiry: timestamppb.New(now.Add(5 * time.Minute)),
   326  				}, []byte("also-secret")),
   327  			}
   328  			_, seenReq, status, resp := call(req, "some-response", nil)
   329  			So(seenReq, ShouldBeNil)
   330  			So(status, ShouldEqual, http.StatusUnauthorized)
   331  			So(resp, ShouldContainSubstring, "failed to verify session token: invalid payload type")
   332  		})
   333  
   334  		Convey("Expired poll token", func() {
   335  			req := testRequest{
   336  				PollToken: genToken(&internalspb.PollState{
   337  					Id:     "poll-state-id",
   338  					Expiry: timestamppb.New(now.Add(-5 * time.Minute)),
   339  				}, []byte("also-secret")),
   340  			}
   341  			_, seenReq, status, resp := call(req, "some-response", nil)
   342  			So(seenReq, ShouldBeNil)
   343  			So(status, ShouldEqual, http.StatusUnauthorized)
   344  			So(resp, ShouldContainSubstring, "no valid poll or state token")
   345  		})
   346  
   347  		Convey("Expired session token", func() {
   348  			req := testRequest{
   349  				SessionToken: genToken(&internalspb.BotSession{
   350  					RbeBotSessionId: "session-id",
   351  					Expiry:          timestamppb.New(now.Add(-5 * time.Minute)),
   352  					PollState:       makePollState("poll-state-id"),
   353  				}, []byte("also-secret")),
   354  			}
   355  			_, seenReq, status, resp := call(req, "some-response", nil)
   356  			So(seenReq, ShouldBeNil)
   357  			So(status, ShouldEqual, http.StatusUnauthorized)
   358  			So(resp, ShouldContainSubstring, "no valid poll or state token")
   359  		})
   360  
   361  		Convey("Expired session and poll tokens", func() {
   362  			req := testRequest{
   363  				PollToken: genToken(&internalspb.PollState{
   364  					Id:     "poll-state-id",
   365  					Expiry: timestamppb.New(now.Add(-5 * time.Minute)),
   366  				}, []byte("also-secret")),
   367  				SessionToken: genToken(&internalspb.BotSession{
   368  					RbeBotSessionId: "session-id",
   369  					Expiry:          timestamppb.New(now.Add(-5 * time.Minute)),
   370  					PollState:       makePollState("poll-state-id"),
   371  				}, []byte("also-secret")),
   372  			}
   373  			_, seenReq, status, resp := call(req, "some-response", nil)
   374  			So(seenReq, ShouldBeNil)
   375  			So(status, ShouldEqual, http.StatusUnauthorized)
   376  			So(resp, ShouldContainSubstring, "no valid poll or state token")
   377  		})
   378  
   379  		Convey("Session token with no session ID", func() {
   380  			req := testRequest{
   381  				SessionToken: genToken(&internalspb.BotSession{
   382  					Expiry:    timestamppb.New(now.Add(5 * time.Minute)),
   383  					PollState: makePollState("poll-state-id"),
   384  				}, []byte("also-secret")),
   385  			}
   386  			_, seenReq, status, resp := call(req, "some-response", nil)
   387  			So(seenReq, ShouldBeNil)
   388  			So(status, ShouldEqual, http.StatusBadRequest)
   389  			So(resp, ShouldContainSubstring, "no session ID")
   390  		})
   391  
   392  		Convey("Poll state dimension overrides", func() {
   393  			pollState := &internalspb.PollState{
   394  				Id:          "poll-state-id",
   395  				Expiry:      timestamppb.New(now.Add(5 * time.Minute)),
   396  				RbeInstance: "correct-rbe-instance",
   397  				EnforcedDimensions: []*internalspb.PollState_Dimension{
   398  					{Key: "id", Values: []string{"correct-bot-id"}},
   399  					{Key: "keep", Values: []string{"a", "b"}},
   400  					{Key: "override-1", Values: []string{"a"}},
   401  					{Key: "override-2", Values: []string{"b", "a"}},
   402  					{Key: "inject", Values: []string{"a"}},
   403  				},
   404  				AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{
   405  					LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{
   406  						MachineFqdn: "bot.fqdn",
   407  					},
   408  				},
   409  			}
   410  
   411  			req := testRequest{
   412  				Dimensions: map[string][]string{
   413  					"id":         {"wrong-bot-id"},
   414  					"pool":       {"pool"},
   415  					"keep":       {"a", "b"},
   416  					"override-1": {"a", "b"},
   417  					"override-2": {"a", "b"},
   418  					"keep-extra": {"a"},
   419  				},
   420  				PollToken: genToken(pollState, []byte("also-secret")),
   421  			}
   422  
   423  			body, seenReq, status, _ := call(req, nil, nil)
   424  			So(status, ShouldEqual, http.StatusOK)
   425  			So(body, ShouldResemble, &testRequest{
   426  				Dimensions: map[string][]string{
   427  					"id":         {"correct-bot-id"},
   428  					"pool":       {"pool"},
   429  					"keep":       {"a", "b"},
   430  					"override-1": {"a"},
   431  					"override-2": {"b", "a"},
   432  					"keep-extra": {"a"},
   433  					"inject":     {"a"},
   434  				},
   435  				PollToken: req.PollToken,
   436  			})
   437  			So(seenReq.BotID, ShouldEqual, "correct-bot-id")
   438  		})
   439  	})
   440  }
   441  
   442  func TestCheckCredentials(t *testing.T) {
   443  	t.Parallel()
   444  
   445  	Convey("No creds", t, func() {
   446  		ctx := auth.WithState(context.Background(), &authtest.FakeState{
   447  			Identity: identity.AnonymousIdentity,
   448  		})
   449  
   450  		err := checkCredentials(ctx, &internalspb.PollState{
   451  			AuthMethod: &internalspb.PollState_GceAuth{
   452  				GceAuth: &internalspb.PollState_GCEAuth{
   453  					GceProject:  "some-project",
   454  					GceInstance: "some-instance",
   455  				},
   456  			},
   457  		})
   458  		So(err, ShouldErrLike, "expecting GCE VM token auth")
   459  
   460  		err = checkCredentials(ctx, &internalspb.PollState{
   461  			AuthMethod: &internalspb.PollState_ServiceAccountAuth_{
   462  				ServiceAccountAuth: &internalspb.PollState_ServiceAccountAuth{
   463  					ServiceAccount: "some-account@example.com",
   464  				},
   465  			},
   466  		})
   467  		So(err, ShouldErrLike, "expecting service account credentials")
   468  
   469  		err = checkCredentials(ctx, &internalspb.PollState{
   470  			AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{
   471  				LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{
   472  					MachineFqdn: "some.fqdn",
   473  				},
   474  			},
   475  		})
   476  		So(err, ShouldErrLike, "expecting LUCI machine token auth")
   477  
   478  		err = checkCredentials(ctx, &internalspb.PollState{
   479  			AuthMethod:  &internalspb.PollState_IpAllowlistAuth{},
   480  			IpAllowlist: "some-ip-allowlist",
   481  		})
   482  		So(err, ShouldErrLike, "is not in the allowlist")
   483  	})
   484  
   485  	Convey("GCE auth", t, func() {
   486  		ctx := auth.WithState(context.Background(), &authtest.FakeState{
   487  			Identity: "bot:ignored",
   488  			UserExtra: &openid.GoogleComputeTokenInfo{
   489  				Project:  "some-project",
   490  				Instance: "some-instance",
   491  			},
   492  		})
   493  
   494  		// OK.
   495  		err := checkCredentials(ctx, &internalspb.PollState{
   496  			AuthMethod: &internalspb.PollState_GceAuth{
   497  				GceAuth: &internalspb.PollState_GCEAuth{
   498  					GceProject:  "some-project",
   499  					GceInstance: "some-instance",
   500  				},
   501  			},
   502  		})
   503  		So(err, ShouldBeNil)
   504  
   505  		// Wrong parameters #1.
   506  		err = checkCredentials(ctx, &internalspb.PollState{
   507  			AuthMethod: &internalspb.PollState_GceAuth{
   508  				GceAuth: &internalspb.PollState_GCEAuth{
   509  					GceProject:  "another-project",
   510  					GceInstance: "some-instance",
   511  				},
   512  			},
   513  		})
   514  		So(err, ShouldErrLike, "wrong GCE VM token")
   515  
   516  		// Wrong parameters #2.
   517  		err = checkCredentials(ctx, &internalspb.PollState{
   518  			AuthMethod: &internalspb.PollState_GceAuth{
   519  				GceAuth: &internalspb.PollState_GCEAuth{
   520  					GceProject:  "some-project",
   521  					GceInstance: "another-instance",
   522  				},
   523  			},
   524  		})
   525  		So(err, ShouldErrLike, "wrong GCE VM token")
   526  	})
   527  
   528  	Convey("Service account auth", t, func() {
   529  		ctx := auth.WithState(context.Background(), &authtest.FakeState{
   530  			Identity: "user:some-account@example.com",
   531  		})
   532  
   533  		// OK.
   534  		err := checkCredentials(ctx, &internalspb.PollState{
   535  			AuthMethod: &internalspb.PollState_ServiceAccountAuth_{
   536  				ServiceAccountAuth: &internalspb.PollState_ServiceAccountAuth{
   537  					ServiceAccount: "some-account@example.com",
   538  				},
   539  			},
   540  		})
   541  		So(err, ShouldBeNil)
   542  
   543  		// Wrong email.
   544  		err = checkCredentials(ctx, &internalspb.PollState{
   545  			AuthMethod: &internalspb.PollState_ServiceAccountAuth_{
   546  				ServiceAccountAuth: &internalspb.PollState_ServiceAccountAuth{
   547  					ServiceAccount: "another-account@example.com",
   548  				},
   549  			},
   550  		})
   551  		So(err, ShouldErrLike, "wrong service account")
   552  	})
   553  
   554  	Convey("Machine token auth", t, func() {
   555  		ctx := auth.WithState(context.Background(), &authtest.FakeState{
   556  			Identity: "bot:ignored",
   557  			UserExtra: &machine.MachineTokenInfo{
   558  				FQDN: "some.fqdn",
   559  			},
   560  		})
   561  
   562  		// OK.
   563  		err := checkCredentials(ctx, &internalspb.PollState{
   564  			AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{
   565  				LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{
   566  					MachineFqdn: "some.fqdn",
   567  				},
   568  			},
   569  		})
   570  		So(err, ShouldBeNil)
   571  
   572  		// Wrong FQDN.
   573  		err = checkCredentials(ctx, &internalspb.PollState{
   574  			AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{
   575  				LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{
   576  					MachineFqdn: "another.fqdn",
   577  				},
   578  			},
   579  		})
   580  		So(err, ShouldErrLike, "wrong FQDN in the LUCI machine token")
   581  	})
   582  
   583  	Convey("IP allowlist", t, func() {
   584  		ctx := auth.WithState(context.Background(), &authtest.FakeState{
   585  			Identity:       identity.AnonymousIdentity,
   586  			PeerIPOverride: net.ParseIP("127.1.1.1"),
   587  			FakeDB: authtest.NewFakeDB(
   588  				authtest.MockIPAllowlist("127.1.1.1", "good"),
   589  				authtest.MockIPAllowlist("127.2.2.2", "bad"),
   590  			),
   591  		})
   592  
   593  		// OK.
   594  		err := checkCredentials(ctx, &internalspb.PollState{
   595  			AuthMethod:  &internalspb.PollState_IpAllowlistAuth{},
   596  			IpAllowlist: "good",
   597  		})
   598  		So(err, ShouldBeNil)
   599  
   600  		// Wrong IP.
   601  		err = checkCredentials(ctx, &internalspb.PollState{
   602  			AuthMethod:  &internalspb.PollState_IpAllowlistAuth{},
   603  			IpAllowlist: "bad",
   604  		})
   605  		So(err, ShouldErrLike, "bot IP 127.1.1.1 is not in the allowlist")
   606  	})
   607  }
   608  
   609  func genToken(msg proto.Message, secret []byte) []byte {
   610  	tok, err := hmactoken.NewStaticSecret(secrets.Secret{Active: secret}).GenerateToken(msg)
   611  	if err != nil {
   612  		panic(err)
   613  	}
   614  	return tok
   615  }