github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/dashboard/app/access_test.go (about) 1 // Copyright 2018 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package main 5 6 import ( 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 "net/http" 12 "strconv" 13 "testing" 14 15 "github.com/google/syzkaller/dashboard/dashapi" 16 "github.com/stretchr/testify/assert" 17 "google.golang.org/appengine/v2/user" 18 ) 19 20 // TestAccessConfig checks that access level were properly assigned throughout the config. 21 func TestAccessConfig(t *testing.T) { 22 config := getConfig(context.Background()) 23 tests := []struct { 24 what string 25 want AccessLevel 26 level AccessLevel 27 }{ 28 {"admin", AccessAdmin, config.Namespaces["access-admin"].AccessLevel}, 29 {"admin/0", AccessAdmin, config.Namespaces["access-admin"].Reporting[0].AccessLevel}, 30 {"admin/1", AccessAdmin, config.Namespaces["access-admin"].Reporting[1].AccessLevel}, 31 {"user", AccessUser, config.Namespaces["access-user"].AccessLevel}, 32 {"user/0", AccessAdmin, config.Namespaces["access-user"].Reporting[0].AccessLevel}, 33 {"user/1", AccessUser, config.Namespaces["access-user"].Reporting[1].AccessLevel}, 34 {"public", AccessPublic, config.Namespaces["access-public"].AccessLevel}, 35 {"public/0", AccessUser, config.Namespaces["access-public"].Reporting[0].AccessLevel}, 36 {"public/1", AccessPublic, config.Namespaces["access-public"].Reporting[1].AccessLevel}, 37 } 38 for _, test := range tests { 39 if test.level != test.want { 40 t.Errorf("%v level %v, want %v", test.what, test.level, test.want) 41 } 42 } 43 } 44 45 // TestAccess checks that all UIs respect access levels. 46 // nolint: funlen, goconst, gocyclo 47 func TestAccess(t *testing.T) { 48 c := NewCtx(t) 49 defer c.Close() 50 51 // entity describes pages/bugs/texts/etc. 52 type entity struct { 53 level AccessLevel // level on which this entity must be visible. 54 ref string // a unique entity reference id. 55 url string // url at which this entity can be requested. 56 } 57 entities := []entity{ 58 // Main pages. 59 { 60 level: AccessAdmin, 61 url: "/admin", 62 }, 63 { 64 level: AccessPublic, 65 url: "/access-public", 66 }, 67 { 68 level: AccessPublic, 69 url: "/access-public/fixed", 70 }, 71 { 72 level: AccessPublic, 73 url: "/access-public/invalid", 74 }, 75 { 76 level: AccessPublic, 77 url: "/access-public/graph/bugs", 78 }, 79 { 80 level: AccessPublic, 81 url: "/access-public/graph/lifetimes", 82 }, 83 { 84 level: AccessPublic, 85 url: "/access-public/graph/fuzzing", 86 }, 87 { 88 level: AccessPublic, 89 url: "/access-public/graph/crashes", 90 }, 91 { 92 level: AccessUser, 93 url: "/access-user", 94 }, 95 { 96 level: AccessUser, 97 url: "/access-user/fixed", 98 }, 99 { 100 level: AccessUser, 101 url: "/access-user/invalid", 102 }, 103 { 104 level: AccessUser, 105 url: "/access-user/graph/bugs", 106 }, 107 { 108 level: AccessUser, 109 url: "/access-user/graph/lifetimes", 110 }, 111 { 112 level: AccessUser, 113 url: "/access-user/graph/fuzzing", 114 }, 115 { 116 level: AccessUser, 117 url: "/access-user/graph/crashes", 118 }, 119 { 120 level: AccessAdmin, 121 url: "/access-admin", 122 }, 123 { 124 level: AccessAdmin, 125 url: "/access-admin/fixed", 126 }, 127 { 128 level: AccessAdmin, 129 url: "/access-admin/invalid", 130 }, 131 { 132 level: AccessAdmin, 133 url: "/access-admin/graph/bugs", 134 }, 135 { 136 level: AccessAdmin, 137 url: "/access-admin/graph/lifetimes", 138 }, 139 { 140 level: AccessAdmin, 141 url: "/access-admin/graph/fuzzing", 142 }, 143 { 144 level: AccessAdmin, 145 url: "/access-admin/graph/crashes", 146 }, 147 { 148 // Any references to namespace, reporting, links, etc. 149 level: AccessUser, 150 ref: "access-user", 151 }, 152 { 153 // Any references to namespace, reporting, links, etc. 154 level: AccessAdmin, 155 ref: "access-admin", 156 }, 157 } 158 159 // noteBugAccessLevel collects all entities associated with the extID bug. 160 noteBugAccessLevel := func(extID string, level, nsLevel AccessLevel) { 161 bug, _, err := findBugByReportingID(c.ctx, extID) 162 c.expectOK(err) 163 crash, _, err := findCrashForBug(c.ctx, bug) 164 c.expectOK(err) 165 bugID := bug.keyHash(c.ctx) 166 entities = append(entities, []entity{ 167 { 168 level: level, 169 ref: bugID, 170 url: fmt.Sprintf("/bug?id=%v", bugID), 171 }, 172 { 173 level: level, 174 ref: bug.Reporting[0].ID, 175 url: fmt.Sprintf("/bug?extid=%v", bug.Reporting[0].ID), 176 }, 177 { 178 level: level, 179 ref: bug.Reporting[1].ID, 180 url: fmt.Sprintf("/bug?extid=%v", bug.Reporting[1].ID), 181 }, 182 { 183 level: level, 184 ref: fmt.Sprint(crash.Log), 185 url: fmt.Sprintf("/text?tag=CrashLog&id=%v", crash.Log), 186 }, 187 { 188 level: level, 189 ref: fmt.Sprint(crash.Log), 190 url: fmt.Sprintf("/text?tag=CrashLog&x=%v", 191 strconv.FormatUint(uint64(crash.Log), 16)), 192 }, 193 { 194 level: level, 195 ref: fmt.Sprint(crash.Report), 196 url: fmt.Sprintf("/text?tag=CrashReport&id=%v", crash.Report), 197 }, 198 { 199 level: level, 200 ref: fmt.Sprint(crash.Report), 201 url: fmt.Sprintf("/text?tag=CrashReport&x=%v", 202 strconv.FormatUint(uint64(crash.Report), 16)), 203 }, 204 { 205 level: level, 206 ref: fmt.Sprint(crash.ReproC), 207 url: fmt.Sprintf("/text?tag=ReproC&id=%v", crash.ReproC), 208 }, 209 { 210 level: level, 211 ref: fmt.Sprint(crash.ReproC), 212 url: fmt.Sprintf("/text?tag=ReproC&x=%v", 213 strconv.FormatUint(uint64(crash.ReproC), 16)), 214 }, 215 { 216 level: level, 217 ref: fmt.Sprint(crash.ReproSyz), 218 url: fmt.Sprintf("/text?tag=ReproSyz&id=%v", crash.ReproSyz), 219 }, 220 { 221 level: level, 222 ref: fmt.Sprint(crash.ReproSyz), 223 url: fmt.Sprintf("/text?tag=ReproSyz&x=%v", 224 strconv.FormatUint(uint64(crash.ReproSyz), 16)), 225 }, 226 { 227 level: level, 228 ref: fmt.Sprint(crash.ReproLog), 229 url: fmt.Sprintf("/text?tag=ReproLog&id=%v", crash.ReproLog), 230 }, 231 { 232 level: level, 233 ref: fmt.Sprint(crash.ReproLog), 234 url: fmt.Sprintf("/text?tag=ReproLog&x=%v", 235 strconv.FormatUint(uint64(crash.ReproLog), 16)), 236 }, 237 { 238 level: nsLevel, 239 ref: fmt.Sprint(crash.MachineInfo), 240 url: fmt.Sprintf("/text?tag=MachineInfo&id=%v", crash.MachineInfo), 241 }, 242 { 243 level: nsLevel, 244 ref: fmt.Sprint(crash.MachineInfo), 245 url: fmt.Sprintf("/text?tag=MachineInfo&x=%v", 246 strconv.FormatUint(uint64(crash.MachineInfo), 16)), 247 }, 248 }...) 249 for _, asset := range crash.Assets { 250 if asset.FsckLog != 0 { 251 entities = append(entities, entity{ 252 level: level, 253 ref: fmt.Sprint(crash.MachineInfo), 254 url: fmt.Sprintf("/x/fsck.log?x=%x", uint64(asset.FsckLog)), 255 }) 256 } 257 } 258 } 259 260 // noteBuildAccessLevel collects all entities associated with the kernel build buildID. 261 noteBuildAccessLevel := func(ns, buildID string) { 262 build, err := loadBuild(c.ctx, ns, buildID) 263 c.expectOK(err) 264 entities = append(entities, entity{ 265 level: c.config().Namespaces[ns].AccessLevel, 266 ref: build.ID, 267 url: fmt.Sprintf("/text?tag=KernelConfig&id=%v", build.KernelConfig), 268 }) 269 } 270 271 // These strings are put into crash log/report, kernel config, etc. 272 // If a request at level UserPublic sees a page containing "access-user", 273 // that will be flagged as error. 274 accessLevelPrefix := func(level AccessLevel) string { 275 switch level { 276 case AccessPublic: 277 return "access-public-" 278 case AccessUser: 279 return "access-user-" 280 default: 281 return "access-admin-" 282 } 283 } 284 285 // For each namespace we create 8 bugs: 286 // invalid, dup, fixed and open for both reportings. 287 // Bugs are setup in such a way that there are lots of 288 // duplicate/similar cross-references. 289 for _, ns := range []string{"access-admin", "access-user", "access-public"} { 290 clientName, clientKey := "", "" 291 for k, v := range c.config().Namespaces[ns].Clients { 292 clientName, clientKey = k, v 293 } 294 nsLevel := c.config().Namespaces[ns].AccessLevel 295 namespaceAccessPrefix := accessLevelPrefix(nsLevel) 296 client := c.makeClient(clientName, clientKey, true) 297 build := testBuild(1) 298 build.KernelConfig = []byte(namespaceAccessPrefix + "build") 299 client.UploadBuild(build) 300 noteBuildAccessLevel(ns, build.ID) 301 302 for reportingIdx := 0; reportingIdx < 2; reportingIdx++ { 303 accessLevel := c.config().Namespaces[ns].Reporting[reportingIdx].AccessLevel 304 accessPrefix := accessLevelPrefix(accessLevel) 305 306 crashInvalid := testCrashWithRepro(build, reportingIdx*10+0) 307 client.ReportCrash(crashInvalid) 308 repInvalid := client.pollBug() 309 if reportingIdx != 0 { 310 client.updateBug(repInvalid.ID, dashapi.BugStatusUpstream, "") 311 repInvalid = client.pollBug() 312 } 313 client.updateBug(repInvalid.ID, dashapi.BugStatusInvalid, "") 314 // Invalid bugs become visible up to the last reporting. 315 finalLevel := c.config().Namespaces[ns]. 316 Reporting[len(c.config().Namespaces[ns].Reporting)-1].AccessLevel 317 noteBugAccessLevel(repInvalid.ID, finalLevel, nsLevel) 318 319 crashFixed := testCrashWithRepro(build, reportingIdx*10+0) 320 client.ReportCrash(crashFixed) 321 repFixed := client.pollBug() 322 if reportingIdx != 0 { 323 client.updateBug(repFixed.ID, dashapi.BugStatusUpstream, "") 324 repFixed = client.pollBug() 325 } 326 reply, _ := client.ReportingUpdate(&dashapi.BugUpdate{ 327 ID: repFixed.ID, 328 Status: dashapi.BugStatusOpen, 329 FixCommits: []string{ns + "-patch0"}, 330 ExtID: accessPrefix + "reporting-ext-id", 331 Link: accessPrefix + "reporting-link", 332 }) 333 c.expectEQ(reply.OK, true) 334 buildFixing := testBuild(reportingIdx*10 + 2) 335 buildFixing.Manager = build.Manager 336 buildFixing.Commits = []string{ns + "-patch0"} 337 client.UploadBuild(buildFixing) 338 noteBuildAccessLevel(ns, buildFixing.ID) 339 // Fixed bugs are also visible up to the last reporting. 340 noteBugAccessLevel(repFixed.ID, finalLevel, nsLevel) 341 342 crashOpen := testCrashWithRepro(build, reportingIdx*10+0) 343 crashOpen.Log = []byte(accessPrefix + "log") 344 crashOpen.Report = []byte(accessPrefix + "report") 345 crashOpen.ReproC = []byte(accessPrefix + "repro c") 346 crashOpen.ReproSyz = []byte(accessPrefix + "repro syz") 347 crashOpen.ReproLog = []byte(accessPrefix + "repro log") 348 crashOpen.MachineInfo = []byte(ns + "machine info") 349 crashOpen.Assets = []dashapi.NewAsset{ 350 { 351 DownloadURL: "http://a.b", 352 Type: dashapi.MountInRepro, 353 FsckLog: []byte("fsck log"), 354 }, 355 } 356 client.ReportCrash(crashOpen) 357 repOpen := client.pollBug() 358 if reportingIdx != 0 { 359 client.updateBug(repOpen.ID, dashapi.BugStatusUpstream, "") 360 repOpen = client.pollBug() 361 } 362 noteBugAccessLevel(repOpen.ID, accessLevel, nsLevel) 363 364 crashPatched := testCrashWithRepro(build, reportingIdx*10+1) 365 client.ReportCrash(crashPatched) 366 repPatched := client.pollBug() 367 if reportingIdx != 0 { 368 client.updateBug(repPatched.ID, dashapi.BugStatusUpstream, "") 369 repPatched = client.pollBug() 370 } 371 reply, _ = client.ReportingUpdate(&dashapi.BugUpdate{ 372 ID: repPatched.ID, 373 Status: dashapi.BugStatusOpen, 374 FixCommits: []string{ns + "-patch0"}, 375 ExtID: accessPrefix + "reporting-ext-id", 376 Link: accessPrefix + "reporting-link", 377 }) 378 c.expectEQ(reply.OK, true) 379 // Patched bugs are also visible up to the last reporting. 380 noteBugAccessLevel(repPatched.ID, finalLevel, nsLevel) 381 382 crashDup := testCrashWithRepro(build, reportingIdx*10+2) 383 client.ReportCrash(crashDup) 384 repDup := client.pollBug() 385 if reportingIdx != 0 { 386 client.updateBug(repDup.ID, dashapi.BugStatusUpstream, "") 387 repDup = client.pollBug() 388 } 389 client.updateBug(repDup.ID, dashapi.BugStatusDup, repOpen.ID) 390 noteBugAccessLevel(repDup.ID, accessLevel, nsLevel) 391 } 392 } 393 394 // checkReferences checks that page contents do not contain 395 // references to entities that must not be visible. 396 checkReferences := func(t *testing.T, url string, requestLevel AccessLevel, reply []byte) { 397 for _, ent := range entities { 398 if requestLevel >= ent.level || ent.ref == "" { 399 continue 400 } 401 if bytes.Contains(reply, []byte(ent.ref)) { 402 t.Errorf("request %v at level %v contains ref %v at level %v:\n%s", 403 url, requestLevel, ent.ref, ent.level, reply) 404 } 405 } 406 } 407 408 // checkPage checks that the page at url is accessible/not accessible as required. 409 checkPage := func(t *testing.T, requestLevel, pageLevel AccessLevel, url string) []byte { 410 reply, err := c.AuthGET(requestLevel, url) 411 if requestLevel >= pageLevel { 412 assert.NoError(t, err) 413 } else if requestLevel == AccessPublic { 414 loginURL, err1 := user.LoginURL(c.ctx, url) 415 if err1 != nil { 416 t.Fatal(err1) 417 } 418 assert.NotNil(t, err) 419 var httpErr *HTTPError 420 assert.True(t, errors.As(err, &httpErr)) 421 assert.Equal(t, httpErr.Code, http.StatusTemporaryRedirect) 422 assert.Equal(t, httpErr.Headers["Location"], []string{loginURL}) 423 } else { 424 expectFailureStatus(t, err, http.StatusForbidden) 425 } 426 return reply 427 } 428 429 // Finally, request all entities at all access levels and 430 // check that we see only what we need to see. 431 for requestLevel := AccessPublic; requestLevel < AccessAdmin; requestLevel++ { 432 for i, ent := range entities { 433 if ent.url == "" { 434 continue 435 } 436 if testing.Short() && (requestLevel != AccessPublic || ent.level == AccessPublic) { 437 // In the short mode, only test that there's no public access to non-public URLs. 438 continue 439 } 440 t.Run(fmt.Sprintf("level%d_%d", requestLevel, i), func(t *testing.T) { 441 reply := checkPage(t, requestLevel, ent.level, ent.url) 442 checkReferences(t, ent.url, requestLevel, reply) 443 }) 444 } 445 } 446 } 447 448 type UserAuthorizationLevel int 449 450 const ( 451 BadAuthDomain UserAuthorizationLevel = iota 452 Regular 453 Authenticated 454 AuthorizedAccessPublic 455 AuthorizedUser 456 AuthorizedAdmin 457 ) 458 459 func makeUser(a UserAuthorizationLevel) *user.User { 460 u := &user.User{} 461 switch a { 462 case BadAuthDomain: 463 u.AuthDomain = "public.com" 464 case Regular: 465 u = nil 466 case Authenticated: 467 u.Email = "someuser@public.com" 468 case AuthorizedAccessPublic: 469 u.Email = "checked-email@public.com" 470 case AuthorizedUser: 471 u.Email = "customer@syzkaller.com" 472 case AuthorizedAdmin: 473 u.Email = "admin@syzkaller.com" 474 u.Admin = true 475 } 476 return u 477 } 478 479 func TestUserAccessLevel(t *testing.T) { 480 tests := []struct { 481 name string 482 u *user.User 483 enforcedAccessLevel string 484 config *GlobalConfig 485 wantAccessLevel AccessLevel 486 wantIsAuthorized bool 487 }{ 488 { 489 name: "wrong auth domain", 490 u: makeUser(BadAuthDomain), 491 wantAccessLevel: AccessPublic, 492 }, 493 { 494 name: "regular not authenticated user", 495 u: makeUser(Regular), 496 wantAccessLevel: AccessPublic, 497 }, 498 { 499 name: "regular not authenticated user wants to be an admin", 500 u: makeUser(Regular), 501 enforcedAccessLevel: "admin", 502 config: testConfig, 503 wantAccessLevel: AccessPublic, 504 }, 505 { 506 name: "regular not authenticated user wants to be a user", 507 u: makeUser(Regular), 508 enforcedAccessLevel: "user", 509 config: testConfig, 510 wantAccessLevel: AccessPublic, 511 }, 512 { 513 name: "authenticated, not authorized user", 514 u: makeUser(Authenticated), 515 config: testConfig, 516 wantAccessLevel: AccessPublic, 517 }, 518 { 519 name: "authenticated, not authorized user wants to be an admin", 520 u: makeUser(Authenticated), 521 enforcedAccessLevel: "admin", 522 config: testConfig, 523 wantAccessLevel: AccessPublic, 524 }, 525 { 526 name: "authenticated, not authorized user wants to be a user", 527 u: makeUser(Authenticated), 528 enforcedAccessLevel: "user", 529 config: testConfig, 530 wantAccessLevel: AccessPublic, 531 }, 532 { 533 name: "authorized for AccessPublic user", 534 u: makeUser(AuthorizedAccessPublic), 535 config: testConfig, 536 wantAccessLevel: AccessPublic, 537 wantIsAuthorized: true, 538 }, 539 { 540 name: "authorized for AccessPublic user wants to be an admin", 541 u: makeUser(AuthorizedAccessPublic), 542 enforcedAccessLevel: "admin", 543 config: testConfig, 544 wantAccessLevel: AccessPublic, 545 wantIsAuthorized: true, 546 }, 547 { 548 name: "authorized for AccessPublic user wants to be a user", 549 u: makeUser(AuthorizedAccessPublic), 550 enforcedAccessLevel: "user", 551 config: testConfig, 552 wantAccessLevel: AccessPublic, 553 wantIsAuthorized: true, 554 }, 555 { 556 name: "authorized for AccessUser user", 557 u: makeUser(AuthorizedUser), 558 config: testConfig, 559 wantAccessLevel: AccessUser, 560 wantIsAuthorized: true, 561 }, 562 { 563 name: "authorized for AccessUser user wants to be an admin", 564 u: makeUser(AuthorizedUser), 565 enforcedAccessLevel: "admin", 566 config: testConfig, 567 wantAccessLevel: AccessUser, 568 wantIsAuthorized: true, 569 }, 570 { 571 name: "authorized admin wants AccessAdmin", 572 u: makeUser(AuthorizedAdmin), 573 config: testConfig, 574 wantAccessLevel: AccessAdmin, 575 wantIsAuthorized: true, 576 }, 577 { 578 name: "authorized admin wants AccessPublic", 579 u: makeUser(AuthorizedAdmin), 580 enforcedAccessLevel: "public", 581 config: testConfig, 582 wantAccessLevel: AccessPublic, 583 wantIsAuthorized: true, 584 }, 585 { 586 name: "authorized admin wants AccessUser", 587 u: makeUser(AuthorizedAdmin), 588 enforcedAccessLevel: "user", 589 config: testConfig, 590 wantAccessLevel: AccessUser, 591 wantIsAuthorized: true, 592 }, 593 } 594 for _, test := range tests { 595 t.Run(test.name, func(t *testing.T) { 596 gotIsAuthorized, gotAccessLevel := userAccessLevel(test.u, test.enforcedAccessLevel, test.config) 597 assert.Equal(t, test.wantAccessLevel, gotAccessLevel) 598 assert.Equal(t, test.wantIsAuthorized, gotIsAuthorized) 599 }) 600 } 601 }