go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/cmd/sidecar/main_test.go (about)

     1  // Copyright 2023 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 main
    16  
    17  import (
    18  	"context"
    19  	"encoding/base64"
    20  	"fmt"
    21  	"testing"
    22  
    23  	statuspb "google.golang.org/genproto/googleapis/rpc/status"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/status"
    26  
    27  	"go.chromium.org/luci/auth/identity"
    28  	"go.chromium.org/luci/common/errors"
    29  	"go.chromium.org/luci/common/proto/sidecar"
    30  	"go.chromium.org/luci/common/retry/transient"
    31  
    32  	"go.chromium.org/luci/server/auth"
    33  	"go.chromium.org/luci/server/auth/authdb"
    34  	"go.chromium.org/luci/server/auth/authtest"
    35  	"go.chromium.org/luci/server/auth/realms"
    36  	"go.chromium.org/luci/server/auth/service/protocol"
    37  
    38  	. "github.com/smartystreets/goconvey/convey"
    39  	. "go.chromium.org/luci/common/testing/assertions"
    40  )
    41  
    42  var (
    43  	testPerm0 = realms.RegisterPermission("fake.permission.0")
    44  	testPerm1 = realms.RegisterPermission("fake.permission.1")
    45  )
    46  
    47  func TestAuthServer(t *testing.T) {
    48  	t.Parallel()
    49  
    50  	Convey("With mocks", t, func() {
    51  		ctx := authtest.MockAuthConfig(context.Background())
    52  		ctx = auth.ModifyConfig(ctx, func(cfg auth.Config) auth.Config {
    53  			cfg.DBProvider = func(ctx context.Context) (authdb.DB, error) {
    54  				return authdb.NewSnapshotDB(&protocol.AuthDB{
    55  					OauthClientId: "Client ID",
    56  					Groups: []*protocol.AuthGroup{
    57  						{
    58  							Name:    auth.InternalServicesGroup,
    59  							Members: []string{"user:service@example.com"},
    60  						},
    61  						{
    62  							Name:    "user-group",
    63  							Members: []string{"user:someone@example.com"},
    64  						},
    65  					},
    66  				}, "http://auth.example.com", 1234, false)
    67  			}
    68  			return cfg
    69  		})
    70  
    71  		srv := &authServerImpl{
    72  			info: &sidecar.ServerInfo{
    73  				SidecarService: "service",
    74  				SidecarJob:     "job",
    75  				SidecarHost:    "host",
    76  				SidecarVersion: "version",
    77  			},
    78  		}
    79  
    80  		expectedInfo := &sidecar.ServerInfo{
    81  			SidecarService: "service",
    82  			SidecarJob:     "job",
    83  			SidecarHost:    "host",
    84  			SidecarVersion: "version",
    85  			AuthDbService:  "http://auth.example.com",
    86  			AuthDbRev:      1234,
    87  		}
    88  
    89  		mockAuthUser := func(u *auth.User) {
    90  			srv.authenticator = auth.Authenticator{
    91  				Methods: []auth.Method{
    92  					authtest.FakeAuth{
    93  						User: u,
    94  					},
    95  				},
    96  			}
    97  		}
    98  
    99  		mockAuthError := func(err error) {
   100  			srv.authenticator = auth.Authenticator{
   101  				Methods: []auth.Method{
   102  					authtest.FakeAuth{
   103  						Error: err,
   104  					},
   105  				},
   106  			}
   107  		}
   108  
   109  		call := func(md ...*sidecar.AuthenticateRequest_Metadata) (*sidecar.AuthenticateResponse, error) {
   110  			return srv.Authenticate(ctx, &sidecar.AuthenticateRequest{
   111  				Protocol: sidecar.AuthenticateRequest_HTTP1,
   112  				Metadata: md,
   113  			})
   114  		}
   115  
   116  		Convey("Anonymous", func() {
   117  			mockAuthUser(&auth.User{Identity: identity.AnonymousIdentity})
   118  			res, err := call()
   119  			So(err, ShouldBeNil)
   120  			So(res, ShouldResembleProto, &sidecar.AuthenticateResponse{
   121  				Identity:   "anonymous:anonymous",
   122  				ServerInfo: expectedInfo,
   123  				Outcome: &sidecar.AuthenticateResponse_Anonymous_{
   124  					Anonymous: &sidecar.AuthenticateResponse_Anonymous{},
   125  				},
   126  			})
   127  		})
   128  
   129  		Convey("User", func() {
   130  			mockAuthUser(&auth.User{
   131  				Identity: "user:someone@example.com",
   132  				Email:    "someone@example.com",
   133  				Name:     "Full Name",
   134  				Picture:  "Picture",
   135  				ClientID: "Client ID",
   136  			})
   137  			res, err := call()
   138  			So(err, ShouldBeNil)
   139  			So(res, ShouldResembleProto, &sidecar.AuthenticateResponse{
   140  				Identity:   "user:someone@example.com",
   141  				ServerInfo: expectedInfo,
   142  				Outcome: &sidecar.AuthenticateResponse_User_{
   143  					User: &sidecar.AuthenticateResponse_User{
   144  						Email:    "someone@example.com",
   145  						Name:     "Full Name",
   146  						Picture:  "Picture",
   147  						ClientId: "Client ID",
   148  					},
   149  				},
   150  			})
   151  		})
   152  
   153  		Convey("Project", func() {
   154  			mockAuthUser(&auth.User{Identity: "user:service@example.com"})
   155  			res, err := call(&sidecar.AuthenticateRequest_Metadata{
   156  				Key:   auth.XLUCIProjectHeader,
   157  				Value: "something",
   158  			})
   159  			So(err, ShouldBeNil)
   160  			So(res, ShouldResembleProto, &sidecar.AuthenticateResponse{
   161  				Identity:   "project:something",
   162  				ServerInfo: expectedInfo,
   163  				Outcome: &sidecar.AuthenticateResponse_Project_{
   164  					Project: &sidecar.AuthenticateResponse_Project{
   165  						Project: "something",
   166  						Service: "user:service@example.com",
   167  					},
   168  				},
   169  			})
   170  		})
   171  
   172  		Convey("Unknown identity kind", func() {
   173  			mockAuthUser(&auth.User{Identity: "bot:what"})
   174  			res, err := call()
   175  			So(err, ShouldBeNil)
   176  			So(res, ShouldResembleProto, &sidecar.AuthenticateResponse{
   177  				Identity:   "anonymous:anonymous",
   178  				ServerInfo: expectedInfo,
   179  				Outcome: &sidecar.AuthenticateResponse_Error{
   180  					Error: &statuspb.Status{
   181  						Code:    int32(codes.Unauthenticated),
   182  						Message: "request was authenticated as \"bot:what\" which is an identity kind not supported by the LUCI Sidecar server",
   183  					},
   184  				},
   185  			})
   186  		})
   187  
   188  		Convey("Fatal auth error", func() {
   189  			mockAuthError(fmt.Errorf("boom"))
   190  			res, err := call()
   191  			So(err, ShouldBeNil)
   192  			So(res, ShouldResembleProto, &sidecar.AuthenticateResponse{
   193  				Identity:   "anonymous:anonymous",
   194  				ServerInfo: expectedInfo,
   195  				Outcome: &sidecar.AuthenticateResponse_Error{
   196  					Error: &statuspb.Status{
   197  						Code:    int32(codes.Unauthenticated),
   198  						Message: "boom",
   199  					},
   200  				},
   201  			})
   202  		})
   203  
   204  		Convey("Fatal auth error with code", func() {
   205  			mockAuthError(status.Errorf(codes.PermissionDenied, "boom"))
   206  			res, err := call()
   207  			So(err, ShouldBeNil)
   208  			So(res, ShouldResembleProto, &sidecar.AuthenticateResponse{
   209  				Identity:   "anonymous:anonymous",
   210  				ServerInfo: expectedInfo,
   211  				Outcome: &sidecar.AuthenticateResponse_Error{
   212  					Error: &statuspb.Status{
   213  						Code:    int32(codes.PermissionDenied),
   214  						Message: "boom",
   215  					},
   216  				},
   217  			})
   218  		})
   219  
   220  		Convey("Transient error", func() {
   221  			mockAuthError(errors.New("boom", transient.Tag))
   222  			_, err := call()
   223  			So(err, ShouldHaveGRPCStatus, codes.Internal)
   224  			So(err, ShouldErrLike, "boom")
   225  		})
   226  
   227  		Convey("Group check", func() {
   228  			mockAuthUser(&auth.User{
   229  				Identity: "user:someone@example.com",
   230  				Email:    "someone@example.com",
   231  			})
   232  			res, err := srv.Authenticate(ctx, &sidecar.AuthenticateRequest{
   233  				Protocol: sidecar.AuthenticateRequest_HTTP1,
   234  				Groups:   []string{"user-group", "something-else"},
   235  			})
   236  			So(err, ShouldBeNil)
   237  			So(res, ShouldResembleProto, &sidecar.AuthenticateResponse{
   238  				Identity:   "user:someone@example.com",
   239  				ServerInfo: expectedInfo,
   240  				Groups:     []string{"user-group"},
   241  				Outcome: &sidecar.AuthenticateResponse_User_{
   242  					User: &sidecar.AuthenticateResponse_User{
   243  						Email: "someone@example.com",
   244  					},
   245  				},
   246  			})
   247  		})
   248  	})
   249  }
   250  
   251  func TestIsMember(t *testing.T) {
   252  	t.Parallel()
   253  
   254  	Convey("With mocks", t, func() {
   255  		ctx := auth.WithState(context.Background(), &authtest.FakeState{
   256  			Identity: "user:sidecar-user-unused@example.com",
   257  			FakeDB: authtest.NewFakeDB(
   258  				authtest.MockMembership("user:enduser@example.com", "group-1"),
   259  				authtest.MockMembership("user:sidecar-user-unused@example.com", "group-2"),
   260  			),
   261  		})
   262  
   263  		srv := &authServerImpl{}
   264  
   265  		Convey("OK", func() {
   266  			res, err := srv.IsMember(ctx, &sidecar.IsMemberRequest{
   267  				Identity: "user:enduser@example.com",
   268  				Groups:   []string{"group-1", "group-2"},
   269  			})
   270  			So(err, ShouldBeNil)
   271  			So(res.IsMember, ShouldBeTrue)
   272  		})
   273  
   274  		Convey("Not a member", func() {
   275  			res, err := srv.IsMember(ctx, &sidecar.IsMemberRequest{
   276  				Identity: "user:enduser@example.com",
   277  				Groups:   []string{"group-2"},
   278  			})
   279  			So(err, ShouldBeNil)
   280  			So(res.IsMember, ShouldBeFalse)
   281  		})
   282  
   283  		Convey("No ident", func() {
   284  			_, err := srv.IsMember(ctx, &sidecar.IsMemberRequest{
   285  				Groups: []string{"group-2"},
   286  			})
   287  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   288  			So(err, ShouldErrLike, "identity field is required")
   289  		})
   290  
   291  		Convey("Bad ident", func() {
   292  			_, err := srv.IsMember(ctx, &sidecar.IsMemberRequest{
   293  				Identity: "what",
   294  				Groups:   []string{"group-2"},
   295  			})
   296  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   297  			So(err, ShouldErrLike, "bad identity")
   298  		})
   299  
   300  		Convey("No groups", func() {
   301  			_, err := srv.IsMember(ctx, &sidecar.IsMemberRequest{
   302  				Identity: "user:enduser@example.com",
   303  			})
   304  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   305  			So(err, ShouldErrLike, "at least one group is required")
   306  		})
   307  	})
   308  }
   309  
   310  func TestHasPermission(t *testing.T) {
   311  	t.Parallel()
   312  
   313  	Convey("With mocks", t, func() {
   314  		ctx := auth.WithState(context.Background(), &authtest.FakeState{
   315  			Identity: "user:sidecar-user-unused@example.com",
   316  			FakeDB: authtest.NewFakeDB(
   317  				authtest.MockPermission("user:enduser@example.com", "test:realm", testPerm0),
   318  				authtest.MockPermission("user:enduser@example.com", "test:realm", testPerm1,
   319  					authtest.RestrictAttribute("test.attr", "good-val"),
   320  				),
   321  			),
   322  		})
   323  
   324  		srv := &authServerImpl{
   325  			perms: map[string]realms.Permission{
   326  				testPerm0.Name(): testPerm0,
   327  				testPerm1.Name(): testPerm1,
   328  			},
   329  		}
   330  
   331  		Convey("OK", func() {
   332  			res, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   333  				Identity:   "user:enduser@example.com",
   334  				Permission: testPerm0.Name(),
   335  				Realm:      "test:realm",
   336  			})
   337  			So(err, ShouldBeNil)
   338  			So(res.HasPermission, ShouldBeTrue)
   339  		})
   340  
   341  		Convey("OK with attrs", func() {
   342  			res, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   343  				Identity:   "user:enduser@example.com",
   344  				Permission: testPerm1.Name(),
   345  				Realm:      "test:realm",
   346  				Attributes: map[string]string{"test.attr": "good-val"},
   347  			})
   348  			So(err, ShouldBeNil)
   349  			So(res.HasPermission, ShouldBeTrue)
   350  		})
   351  
   352  		Convey("No permission", func() {
   353  			res, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   354  				Identity:   "user:enduser@example.com",
   355  				Permission: testPerm1.Name(),
   356  				Realm:      "test:realm",
   357  			})
   358  			So(err, ShouldBeNil)
   359  			So(res.HasPermission, ShouldBeFalse)
   360  		})
   361  
   362  		Convey("No ident", func() {
   363  			_, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   364  				Permission: testPerm0.Name(),
   365  				Realm:      "test:realm",
   366  			})
   367  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   368  			So(err, ShouldErrLike, "identity field is required")
   369  		})
   370  
   371  		Convey("Bad ident", func() {
   372  			_, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   373  				Identity:   "what",
   374  				Permission: testPerm0.Name(),
   375  				Realm:      "test:realm",
   376  			})
   377  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   378  			So(err, ShouldErrLike, "bad identity")
   379  		})
   380  
   381  		Convey("No perm", func() {
   382  			_, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   383  				Identity: "user:enduser@example.com",
   384  				Realm:    "test:realm",
   385  			})
   386  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   387  			So(err, ShouldErrLike, "permission field is required")
   388  		})
   389  
   390  		Convey("Bad perm", func() {
   391  			_, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   392  				Identity:   "user:enduser@example.com",
   393  				Permission: "what",
   394  				Realm:      "test:realm",
   395  			})
   396  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   397  			So(err, ShouldErrLike, "bad permission")
   398  		})
   399  
   400  		Convey("Unknown perm", func() {
   401  			_, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   402  				Identity:   "user:enduser@example.com",
   403  				Permission: "fake.permission.unknown",
   404  				Realm:      "test:realm",
   405  			})
   406  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   407  			So(err, ShouldErrLike, "is not registered")
   408  		})
   409  
   410  		Convey("No realm", func() {
   411  			_, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   412  				Identity:   "user:enduser@example.com",
   413  				Permission: testPerm0.Name(),
   414  			})
   415  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   416  			So(err, ShouldErrLike, "realm field is required")
   417  		})
   418  
   419  		Convey("Bad realm", func() {
   420  			_, err := srv.HasPermission(ctx, &sidecar.HasPermissionRequest{
   421  				Identity:   "user:enduser@example.com",
   422  				Permission: testPerm0.Name(),
   423  				Realm:      "bad",
   424  			})
   425  			So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   426  			So(err, ShouldErrLike, "bad global realm name")
   427  		})
   428  	})
   429  }
   430  
   431  func TestRequestMetadata(t *testing.T) {
   432  	t.Parallel()
   433  
   434  	Convey("HTTP", t, func() {
   435  		req, err := newRequestMetadata(&sidecar.AuthenticateRequest{
   436  			Protocol: sidecar.AuthenticateRequest_HTTP1,
   437  			Metadata: []*sidecar.AuthenticateRequest_Metadata{
   438  				{Key: "Header", Value: "Val1"},
   439  				{Key: "HeaDer", Value: "Val2"},
   440  				{Key: "Host", Value: "host"},
   441  				{Key: "Header-Bin", Value: "val"},
   442  				{Key: "Cookie", Value: "cookie_1=value_1; cookie_2=value_2"},
   443  				{Key: "Cookie", Value: "cookie_3=value_3"},
   444  			},
   445  		})
   446  		So(err, ShouldBeNil)
   447  		So(req.Host(), ShouldEqual, "host")
   448  		So(req.RemoteAddr(), ShouldEqual, "")
   449  		So(req.Header("HEADER"), ShouldEqual, "Val1")
   450  		So(req.Header("header-bin"), ShouldEqual, "val")
   451  
   452  		cookie, err := req.Cookie("cookie_3")
   453  		So(err, ShouldBeNil)
   454  		So(cookie.Value, ShouldEqual, "value_3")
   455  	})
   456  
   457  	Convey("gRPC", t, func() {
   458  		req, err := newRequestMetadata(&sidecar.AuthenticateRequest{
   459  			Protocol: sidecar.AuthenticateRequest_GRPC,
   460  			Metadata: []*sidecar.AuthenticateRequest_Metadata{
   461  				{Key: ":authority", Value: "host"},
   462  				{Key: "Header-Bin", Value: base64.RawStdEncoding.EncodeToString([]byte("val"))},
   463  			},
   464  		})
   465  		So(err, ShouldBeNil)
   466  		So(req.Host(), ShouldEqual, "host")
   467  		So(req.Header("header-bin"), ShouldEqual, "val")
   468  	})
   469  
   470  	Convey("Unknown protocol", t, func() {
   471  		_, err := newRequestMetadata(&sidecar.AuthenticateRequest{})
   472  		So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   473  	})
   474  }