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  }