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 }