github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/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 "google.golang.org/appengine/v2/user" 17 ) 18 19 // TestAccessConfig checks that access level were properly assigned throughout the config. 20 func TestAccessConfig(t *testing.T) { 21 config := getConfig(context.Background()) 22 tests := []struct { 23 what string 24 want AccessLevel 25 level AccessLevel 26 }{ 27 {"admin", AccessAdmin, config.Namespaces["access-admin"].AccessLevel}, 28 {"admin/0", AccessAdmin, config.Namespaces["access-admin"].Reporting[0].AccessLevel}, 29 {"admin/1", AccessAdmin, config.Namespaces["access-admin"].Reporting[1].AccessLevel}, 30 {"user", AccessUser, config.Namespaces["access-user"].AccessLevel}, 31 {"user/0", AccessAdmin, config.Namespaces["access-user"].Reporting[0].AccessLevel}, 32 {"user/1", AccessUser, config.Namespaces["access-user"].Reporting[1].AccessLevel}, 33 {"public", AccessPublic, config.Namespaces["access-public"].AccessLevel}, 34 {"public/0", AccessUser, config.Namespaces["access-public"].Reporting[0].AccessLevel}, 35 {"public/1", AccessPublic, config.Namespaces["access-public"].Reporting[1].AccessLevel}, 36 } 37 for _, test := range tests { 38 if test.level != test.want { 39 t.Errorf("%v level %v, want %v", test.what, test.level, test.want) 40 } 41 } 42 } 43 44 // TestAccess checks that all UIs respect access levels. 45 // nolint: funlen, goconst 46 func TestAccess(t *testing.T) { 47 if testing.Short() { 48 t.Skip() 49 } 50 c := NewCtx(t) 51 defer c.Close() 52 53 // entity describes pages/bugs/texts/etc. 54 type entity struct { 55 level AccessLevel // level on which this entity must be visible. 56 ref string // a unique entity reference id. 57 url string // url at which this entity can be requested. 58 } 59 entities := []entity{ 60 // Main pages. 61 { 62 level: AccessAdmin, 63 url: "/admin", 64 }, 65 { 66 level: AccessPublic, 67 url: "/access-public", 68 }, 69 { 70 level: AccessPublic, 71 url: "/access-public/fixed", 72 }, 73 { 74 level: AccessPublic, 75 url: "/access-public/invalid", 76 }, 77 { 78 level: AccessPublic, 79 url: "/access-public/graph/bugs", 80 }, 81 { 82 level: AccessPublic, 83 url: "/access-public/graph/lifetimes", 84 }, 85 { 86 level: AccessPublic, 87 url: "/access-public/graph/fuzzing", 88 }, 89 { 90 level: AccessPublic, 91 url: "/access-public/graph/crashes", 92 }, 93 { 94 level: AccessUser, 95 url: "/access-user", 96 }, 97 { 98 level: AccessUser, 99 url: "/access-user/fixed", 100 }, 101 { 102 level: AccessUser, 103 url: "/access-user/invalid", 104 }, 105 { 106 level: AccessUser, 107 url: "/access-user/graph/bugs", 108 }, 109 { 110 level: AccessUser, 111 url: "/access-user/graph/lifetimes", 112 }, 113 { 114 level: AccessUser, 115 url: "/access-user/graph/fuzzing", 116 }, 117 { 118 level: AccessUser, 119 url: "/access-user/graph/crashes", 120 }, 121 { 122 level: AccessAdmin, 123 url: "/access-admin", 124 }, 125 { 126 level: AccessAdmin, 127 url: "/access-admin/fixed", 128 }, 129 { 130 level: AccessAdmin, 131 url: "/access-admin/invalid", 132 }, 133 { 134 level: AccessAdmin, 135 url: "/access-admin/graph/bugs", 136 }, 137 { 138 level: AccessAdmin, 139 url: "/access-admin/graph/lifetimes", 140 }, 141 { 142 level: AccessAdmin, 143 url: "/access-admin/graph/fuzzing", 144 }, 145 { 146 level: AccessAdmin, 147 url: "/access-admin/graph/crashes", 148 }, 149 { 150 // Any references to namespace, reporting, links, etc. 151 level: AccessUser, 152 ref: "access-user", 153 }, 154 { 155 // Any references to namespace, reporting, links, etc. 156 level: AccessAdmin, 157 ref: "access-admin", 158 }, 159 } 160 161 // noteBugAccessLevel collects all entities associated with the extID bug. 162 noteBugAccessLevel := func(extID string, level, nsLevel AccessLevel) { 163 bug, _, err := findBugByReportingID(c.ctx, extID) 164 c.expectOK(err) 165 crash, _, err := findCrashForBug(c.ctx, bug) 166 c.expectOK(err) 167 bugID := bug.keyHash(c.ctx) 168 entities = append(entities, []entity{ 169 { 170 level: level, 171 ref: bugID, 172 url: fmt.Sprintf("/bug?id=%v", bugID), 173 }, 174 { 175 level: level, 176 ref: bug.Reporting[0].ID, 177 url: fmt.Sprintf("/bug?extid=%v", bug.Reporting[0].ID), 178 }, 179 { 180 level: level, 181 ref: bug.Reporting[1].ID, 182 url: fmt.Sprintf("/bug?extid=%v", bug.Reporting[1].ID), 183 }, 184 { 185 level: level, 186 ref: fmt.Sprint(crash.Log), 187 url: fmt.Sprintf("/text?tag=CrashLog&id=%v", crash.Log), 188 }, 189 { 190 level: level, 191 ref: fmt.Sprint(crash.Log), 192 url: fmt.Sprintf("/text?tag=CrashLog&x=%v", 193 strconv.FormatUint(uint64(crash.Log), 16)), 194 }, 195 { 196 level: level, 197 ref: fmt.Sprint(crash.Report), 198 url: fmt.Sprintf("/text?tag=CrashReport&id=%v", crash.Report), 199 }, 200 { 201 level: level, 202 ref: fmt.Sprint(crash.Report), 203 url: fmt.Sprintf("/text?tag=CrashReport&x=%v", 204 strconv.FormatUint(uint64(crash.Report), 16)), 205 }, 206 { 207 level: level, 208 ref: fmt.Sprint(crash.ReproC), 209 url: fmt.Sprintf("/text?tag=ReproC&id=%v", crash.ReproC), 210 }, 211 { 212 level: level, 213 ref: fmt.Sprint(crash.ReproC), 214 url: fmt.Sprintf("/text?tag=ReproC&x=%v", 215 strconv.FormatUint(uint64(crash.ReproC), 16)), 216 }, 217 { 218 level: level, 219 ref: fmt.Sprint(crash.ReproSyz), 220 url: fmt.Sprintf("/text?tag=ReproSyz&id=%v", crash.ReproSyz), 221 }, 222 { 223 level: level, 224 ref: fmt.Sprint(crash.ReproSyz), 225 url: fmt.Sprintf("/text?tag=ReproSyz&x=%v", 226 strconv.FormatUint(uint64(crash.ReproSyz), 16)), 227 }, 228 { 229 level: nsLevel, 230 ref: fmt.Sprint(crash.MachineInfo), 231 url: fmt.Sprintf("/text?tag=MachineInfo&id=%v", crash.MachineInfo), 232 }, 233 { 234 level: nsLevel, 235 ref: fmt.Sprint(crash.MachineInfo), 236 url: fmt.Sprintf("/text?tag=MachineInfo&x=%v", 237 strconv.FormatUint(uint64(crash.MachineInfo), 16)), 238 }, 239 }...) 240 } 241 242 // noteBuildAccessLevel collects all entities associated with the kernel build buildID. 243 noteBuildAccessLevel := func(ns, buildID string) { 244 build, err := loadBuild(c.ctx, ns, buildID) 245 c.expectOK(err) 246 entities = append(entities, entity{ 247 level: c.config().Namespaces[ns].AccessLevel, 248 ref: build.ID, 249 url: fmt.Sprintf("/text?tag=KernelConfig&id=%v", build.KernelConfig), 250 }) 251 } 252 253 // These strings are put into crash log/report, kernel config, etc. 254 // If a request at level UserPublic sees a page containing "access-user", 255 // that will be flagged as error. 256 accessLevelPrefix := func(level AccessLevel) string { 257 switch level { 258 case AccessPublic: 259 return "access-public-" 260 case AccessUser: 261 return "access-user-" 262 default: 263 return "access-admin-" 264 } 265 } 266 267 // For each namespace we create 8 bugs: 268 // invalid, dup, fixed and open for both reportings. 269 // Bugs are setup in such a way that there are lots of 270 // duplicate/similar cross-references. 271 for _, ns := range []string{"access-admin", "access-user", "access-public"} { 272 clientName, clientKey := "", "" 273 for k, v := range c.config().Namespaces[ns].Clients { 274 clientName, clientKey = k, v 275 } 276 nsLevel := c.config().Namespaces[ns].AccessLevel 277 namespaceAccessPrefix := accessLevelPrefix(nsLevel) 278 client := c.makeClient(clientName, clientKey, true) 279 build := testBuild(1) 280 build.KernelConfig = []byte(namespaceAccessPrefix + "build") 281 client.UploadBuild(build) 282 noteBuildAccessLevel(ns, build.ID) 283 284 for reportingIdx := 0; reportingIdx < 2; reportingIdx++ { 285 accessLevel := c.config().Namespaces[ns].Reporting[reportingIdx].AccessLevel 286 accessPrefix := accessLevelPrefix(accessLevel) 287 288 crashInvalid := testCrashWithRepro(build, reportingIdx*10+0) 289 client.ReportCrash(crashInvalid) 290 repInvalid := client.pollBug() 291 if reportingIdx != 0 { 292 client.updateBug(repInvalid.ID, dashapi.BugStatusUpstream, "") 293 repInvalid = client.pollBug() 294 } 295 client.updateBug(repInvalid.ID, dashapi.BugStatusInvalid, "") 296 // Invalid bugs become visible up to the last reporting. 297 finalLevel := c.config().Namespaces[ns]. 298 Reporting[len(c.config().Namespaces[ns].Reporting)-1].AccessLevel 299 noteBugAccessLevel(repInvalid.ID, finalLevel, nsLevel) 300 301 crashFixed := testCrashWithRepro(build, reportingIdx*10+0) 302 client.ReportCrash(crashFixed) 303 repFixed := client.pollBug() 304 if reportingIdx != 0 { 305 client.updateBug(repFixed.ID, dashapi.BugStatusUpstream, "") 306 repFixed = client.pollBug() 307 } 308 reply, _ := client.ReportingUpdate(&dashapi.BugUpdate{ 309 ID: repFixed.ID, 310 Status: dashapi.BugStatusOpen, 311 FixCommits: []string{ns + "-patch0"}, 312 ExtID: accessPrefix + "reporting-ext-id", 313 Link: accessPrefix + "reporting-link", 314 }) 315 c.expectEQ(reply.OK, true) 316 buildFixing := testBuild(reportingIdx*10 + 2) 317 buildFixing.Manager = build.Manager 318 buildFixing.Commits = []string{ns + "-patch0"} 319 client.UploadBuild(buildFixing) 320 noteBuildAccessLevel(ns, buildFixing.ID) 321 // Fixed bugs are also visible up to the last reporting. 322 noteBugAccessLevel(repFixed.ID, finalLevel, nsLevel) 323 324 crashOpen := testCrashWithRepro(build, reportingIdx*10+0) 325 crashOpen.Log = []byte(accessPrefix + "log") 326 crashOpen.Report = []byte(accessPrefix + "report") 327 crashOpen.ReproC = []byte(accessPrefix + "repro c") 328 crashOpen.ReproSyz = []byte(accessPrefix + "repro syz") 329 crashOpen.MachineInfo = []byte(ns + "machine info") 330 client.ReportCrash(crashOpen) 331 repOpen := client.pollBug() 332 if reportingIdx != 0 { 333 client.updateBug(repOpen.ID, dashapi.BugStatusUpstream, "") 334 repOpen = client.pollBug() 335 } 336 noteBugAccessLevel(repOpen.ID, accessLevel, nsLevel) 337 338 crashPatched := testCrashWithRepro(build, reportingIdx*10+1) 339 client.ReportCrash(crashPatched) 340 repPatched := client.pollBug() 341 if reportingIdx != 0 { 342 client.updateBug(repPatched.ID, dashapi.BugStatusUpstream, "") 343 repPatched = client.pollBug() 344 } 345 reply, _ = client.ReportingUpdate(&dashapi.BugUpdate{ 346 ID: repPatched.ID, 347 Status: dashapi.BugStatusOpen, 348 FixCommits: []string{ns + "-patch0"}, 349 ExtID: accessPrefix + "reporting-ext-id", 350 Link: accessPrefix + "reporting-link", 351 }) 352 c.expectEQ(reply.OK, true) 353 // Patched bugs are also visible up to the last reporting. 354 noteBugAccessLevel(repPatched.ID, finalLevel, nsLevel) 355 356 crashDup := testCrashWithRepro(build, reportingIdx*10+2) 357 client.ReportCrash(crashDup) 358 repDup := client.pollBug() 359 if reportingIdx != 0 { 360 client.updateBug(repDup.ID, dashapi.BugStatusUpstream, "") 361 repDup = client.pollBug() 362 } 363 client.updateBug(repDup.ID, dashapi.BugStatusDup, repOpen.ID) 364 noteBugAccessLevel(repDup.ID, accessLevel, nsLevel) 365 } 366 } 367 368 // checkReferences checks that page contents do not contain 369 // references to entities that must not be visible. 370 checkReferences := func(url string, requestLevel AccessLevel, reply []byte) { 371 for _, ent := range entities { 372 if requestLevel >= ent.level || ent.ref == "" { 373 continue 374 } 375 if bytes.Contains(reply, []byte(ent.ref)) { 376 t.Errorf("request %v at level %v contains ref %v at level %v:\n%s", 377 url, requestLevel, ent.ref, ent.level, reply) 378 } 379 } 380 } 381 382 // checkPage checks that the page at url is accessible/not accessible as required. 383 checkPage := func(requestLevel, pageLevel AccessLevel, url string) []byte { 384 reply, err := c.AuthGET(requestLevel, url) 385 if requestLevel >= pageLevel { 386 c.expectOK(err) 387 } else if requestLevel == AccessPublic { 388 loginURL, err1 := user.LoginURL(c.ctx, url) 389 if err1 != nil { 390 t.Fatal(err1) 391 } 392 c.expectNE(err, nil) 393 var httpErr *HTTPError 394 c.expectTrue(errors.As(err, &httpErr)) 395 c.expectEQ(httpErr.Code, http.StatusTemporaryRedirect) 396 c.expectEQ(httpErr.Headers["Location"], []string{loginURL}) 397 } else { 398 c.expectForbidden(err) 399 } 400 return reply 401 } 402 403 // Finally, request all entities at all access levels and 404 // check that we see only what we need to see. 405 for requestLevel := AccessPublic; requestLevel < AccessAdmin; requestLevel++ { 406 for _, ent := range entities { 407 if ent.url == "" { 408 continue 409 } 410 reply := checkPage(requestLevel, ent.level, ent.url) 411 checkReferences(ent.url, requestLevel, reply) 412 } 413 } 414 }