blob: 10afa01c2718fa30135c6f70d2b215b7f272b9f7 [file] [log] [blame]
// Copyright 2018 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
// +build aetest
package dash
import (
"bytes"
"fmt"
"net/http"
"strconv"
"testing"
"github.com/google/syzkaller/dashboard/dashapi"
"google.golang.org/appengine/user"
)
// TestAccessConfig checks that access level were properly assigned throughout the config.
func TestAccessConfig(t *testing.T) {
tests := []struct {
what string
want AccessLevel
level AccessLevel
}{
{"admin", AccessAdmin, config.Namespaces["access-admin"].AccessLevel},
{"admin/0", AccessAdmin, config.Namespaces["access-admin"].Reporting[0].AccessLevel},
{"admin/1", AccessAdmin, config.Namespaces["access-admin"].Reporting[1].AccessLevel},
{"user", AccessUser, config.Namespaces["access-user"].AccessLevel},
{"user/0", AccessAdmin, config.Namespaces["access-user"].Reporting[0].AccessLevel},
{"user/1", AccessUser, config.Namespaces["access-user"].Reporting[1].AccessLevel},
{"public", AccessPublic, config.Namespaces["access-public"].AccessLevel},
{"public/0", AccessUser, config.Namespaces["access-public"].Reporting[0].AccessLevel},
{"public/1", AccessPublic, config.Namespaces["access-public"].Reporting[1].AccessLevel},
}
for _, test := range tests {
if test.level != test.want {
t.Errorf("%v level %v, want %v", test.what, test.level, test.want)
}
}
}
// TestAccess checks that all UIs respect access levels.
func TestAccess(t *testing.T) {
if testing.Short() {
t.Skip()
}
c := NewCtx(t)
defer c.Close()
// entity describes pages/bugs/texts/etc.
type entity struct {
level AccessLevel // level on which this entity must be visible.
ref string // a unique entity reference id.
url string // url at which this entity can be requested.
}
entities := []entity{
// Main pages.
{
level: AccessAdmin,
url: "/admin",
},
{
level: AccessPublic,
url: "/access-public",
},
{
level: AccessPublic,
url: "/access-public/fixed",
},
{
level: AccessPublic,
url: "/access-public/invalid",
},
{
level: AccessUser,
url: "/access-user",
},
{
level: AccessUser,
url: "/access-user/fixed",
},
{
level: AccessUser,
url: "/access-user/invalid",
},
{
level: AccessAdmin,
url: "/access-admin",
},
{
level: AccessAdmin,
url: "/access-admin/fixed",
},
{
level: AccessAdmin,
url: "/access-admin/invalid",
},
{
// Any references to namespace, reporting, links, etc.
level: AccessUser,
ref: "access-user",
},
{
// Any references to namespace, reporting, links, etc.
level: AccessAdmin,
ref: "access-admin",
},
}
// noteBugAccessLevel collects all entities associated with the extID bug.
noteBugAccessLevel := func(extID string, level AccessLevel) {
bug, _, err := findBugByReportingID(c.ctx, extID)
c.expectOK(err)
crash, _, err := findCrashForBug(c.ctx, bug)
c.expectOK(err)
bugID := bug.keyHash()
entities = append(entities, []entity{
{
level: level,
ref: bugID,
url: fmt.Sprintf("/bug?id=%v", bugID),
},
{
level: level,
ref: bug.Reporting[0].ID,
url: fmt.Sprintf("/bug?extid=%v", bug.Reporting[0].ID),
},
{
level: level,
ref: bug.Reporting[1].ID,
url: fmt.Sprintf("/bug?extid=%v", bug.Reporting[1].ID),
},
{
level: level,
ref: fmt.Sprint(crash.Log),
url: fmt.Sprintf("/text?tag=CrashLog&id=%v", crash.Log),
},
{
level: level,
ref: fmt.Sprint(crash.Log),
url: fmt.Sprintf("/text?tag=CrashLog&x=%v",
strconv.FormatUint(uint64(crash.Log), 16)),
},
{
level: level,
ref: fmt.Sprint(crash.Report),
url: fmt.Sprintf("/text?tag=CrashReport&id=%v", crash.Report),
},
{
level: level,
ref: fmt.Sprint(crash.Report),
url: fmt.Sprintf("/text?tag=CrashReport&x=%v",
strconv.FormatUint(uint64(crash.Report), 16)),
},
{
level: level,
ref: fmt.Sprint(crash.ReproC),
url: fmt.Sprintf("/text?tag=ReproC&id=%v", crash.ReproC),
},
{
level: level,
ref: fmt.Sprint(crash.ReproC),
url: fmt.Sprintf("/text?tag=ReproC&x=%v",
strconv.FormatUint(uint64(crash.ReproC), 16)),
},
{
level: level,
ref: fmt.Sprint(crash.ReproSyz),
url: fmt.Sprintf("/text?tag=ReproSyz&id=%v", crash.ReproSyz),
},
{
level: level,
ref: fmt.Sprint(crash.ReproSyz),
url: fmt.Sprintf("/text?tag=ReproSyz&x=%v",
strconv.FormatUint(uint64(crash.ReproSyz), 16)),
},
}...)
}
// noteBuildccessLevel collects all entities associated with the kernel build buildID.
noteBuildccessLevel := func(ns, buildID string) {
build, err := loadBuild(c.ctx, ns, buildID)
c.expectOK(err)
entities = append(entities, entity{
level: config.Namespaces[ns].AccessLevel,
ref: build.ID,
url: fmt.Sprintf("/text?tag=KernelConfig&id=%v", build.KernelConfig),
})
}
// These strings are put into crash log/report, kernel config, etc.
// If a request at level UserPublic sees a page containing "access-user",
// that will be flagged as error.
accessLevelPrefix := func(level AccessLevel) string {
switch level {
case AccessPublic:
return "access-public-"
case AccessUser:
return "access-user-"
default:
return "access-admin-"
}
}
// For each namespace we create 8 bugs:
// invalid, dup, fixed and open for both reportings.
// Bugs are setup in such a way that there are lots of
// duplicate/similar cross-references.
for _, ns := range []string{"access-admin", "access-user", "access-public"} {
clientName, clientKey := "", ""
for k, v := range config.Namespaces[ns].Clients {
clientName, clientKey = k, v
}
namespaceAccessPrefix := accessLevelPrefix(config.Namespaces[ns].AccessLevel)
client := c.makeClient(clientName, clientKey, true)
build := testBuild(1)
build.KernelConfig = []byte(namespaceAccessPrefix + "build")
client.UploadBuild(build)
noteBuildccessLevel(ns, build.ID)
for reportingIdx := 0; reportingIdx < 2; reportingIdx++ {
accessLevel := config.Namespaces[ns].Reporting[reportingIdx].AccessLevel
accessPrefix := accessLevelPrefix(accessLevel)
crashInvalid := testCrashWithRepro(build, reportingIdx*10+0)
client.ReportCrash(crashInvalid)
repInvalid := client.pollBug()
if reportingIdx != 0 {
client.updateBug(repInvalid.ID, dashapi.BugStatusUpstream, "")
repInvalid = client.pollBug()
}
client.updateBug(repInvalid.ID, dashapi.BugStatusInvalid, "")
// Invalid bugs become visible up to the last reporting.
finalLevel := config.Namespaces[ns].
Reporting[len(config.Namespaces[ns].Reporting)-1].AccessLevel
noteBugAccessLevel(repInvalid.ID, finalLevel)
crashFixed := testCrashWithRepro(build, reportingIdx*10+0)
client.ReportCrash(crashFixed)
repFixed := client.pollBug()
if reportingIdx != 0 {
client.updateBug(repFixed.ID, dashapi.BugStatusUpstream, "")
repFixed = client.pollBug()
}
reply, _ := client.ReportingUpdate(&dashapi.BugUpdate{
ID: repFixed.ID,
Status: dashapi.BugStatusOpen,
FixCommits: []string{ns + "-patch0"},
ExtID: accessPrefix + "reporting-ext-id",
Link: accessPrefix + "reporting-link",
})
c.expectEQ(reply.OK, true)
buildFixing := testBuild(reportingIdx*10 + 2)
buildFixing.Manager = build.Manager
buildFixing.Commits = []string{ns + "-patch0"}
client.UploadBuild(buildFixing)
noteBuildccessLevel(ns, buildFixing.ID)
// Fixed bugs are also visible up to the last reporting.
noteBugAccessLevel(repFixed.ID, finalLevel)
crashOpen := testCrashWithRepro(build, reportingIdx*10+0)
crashOpen.Log = []byte(accessPrefix + "log")
crashOpen.Report = []byte(accessPrefix + "report")
crashOpen.ReproC = []byte(accessPrefix + "repro c")
crashOpen.ReproSyz = []byte(accessPrefix + "repro syz")
client.ReportCrash(crashOpen)
repOpen := client.pollBug()
if reportingIdx != 0 {
client.updateBug(repOpen.ID, dashapi.BugStatusUpstream, "")
repOpen = client.pollBug()
}
noteBugAccessLevel(repOpen.ID, accessLevel)
crashPatched := testCrashWithRepro(build, reportingIdx*10+1)
client.ReportCrash(crashPatched)
repPatched := client.pollBug()
if reportingIdx != 0 {
client.updateBug(repPatched.ID, dashapi.BugStatusUpstream, "")
repPatched = client.pollBug()
}
reply, _ = client.ReportingUpdate(&dashapi.BugUpdate{
ID: repPatched.ID,
Status: dashapi.BugStatusOpen,
FixCommits: []string{ns + "-patch0"},
ExtID: accessPrefix + "reporting-ext-id",
Link: accessPrefix + "reporting-link",
})
c.expectEQ(reply.OK, true)
// Patched bugs are also visible up to the last reporting.
noteBugAccessLevel(repPatched.ID, finalLevel)
crashDup := testCrashWithRepro(build, reportingIdx*10+2)
client.ReportCrash(crashDup)
repDup := client.pollBug()
if reportingIdx != 0 {
client.updateBug(repDup.ID, dashapi.BugStatusUpstream, "")
repDup = client.pollBug()
}
client.updateBug(repDup.ID, dashapi.BugStatusDup, repOpen.ID)
noteBugAccessLevel(repDup.ID, accessLevel)
}
}
// checkReferences checks that page contents do not contain
// references to entities that must not be visible.
checkReferences := func(url string, requestLevel AccessLevel, reply []byte) {
for _, ent := range entities {
if requestLevel >= ent.level || ent.ref == "" {
continue
}
if bytes.Contains(reply, []byte(ent.ref)) {
t.Errorf("request %v at level %v contains ref %v at level %v:\n%s\n\n",
url, requestLevel, ent.ref, ent.level, reply)
}
}
}
// checkPage checks that the page at url is accessible/not accessible as required.
checkPage := func(requestLevel, pageLevel AccessLevel, url string) []byte {
reply, err := c.AuthGET(requestLevel, url)
if requestLevel >= pageLevel {
c.expectOK(err)
} else if requestLevel == AccessPublic {
loginURL, err1 := user.LoginURL(c.ctx, url)
if err1 != nil {
t.Fatal(err1)
}
c.expectNE(err, nil)
httpErr, ok := err.(HttpError)
c.expectTrue(ok)
c.expectEQ(httpErr.Code, http.StatusTemporaryRedirect)
c.expectEQ(httpErr.Headers["Location"], []string{loginURL})
} else {
c.expectForbidden(err)
}
return reply
}
// Finally, request all entities at all access levels and
// check that we see only what we need to see.
for requestLevel := AccessPublic; requestLevel < AccessAdmin; requestLevel++ {
for _, ent := range entities {
if ent.url == "" {
continue
}
reply := checkPage(requestLevel, ent.level, ent.url)
checkReferences(ent.url, requestLevel, reply)
}
}
}