github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/access.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 "context" 8 "errors" 9 "fmt" 10 "net/http" 11 "strings" 12 13 db "google.golang.org/appengine/v2/datastore" 14 "google.golang.org/appengine/v2/log" 15 "google.golang.org/appengine/v2/user" 16 ) 17 18 type AccessLevel int 19 20 const ( 21 AccessPublic AccessLevel = iota + 1 22 AccessUser 23 AccessAdmin 24 ) 25 26 func verifyAccessLevel(access AccessLevel) { 27 switch access { 28 case AccessPublic, AccessUser, AccessAdmin: 29 return 30 default: 31 panic(fmt.Sprintf("bad access level %v", access)) 32 } 33 } 34 35 var ErrAccess = errors.New("unauthorized") 36 37 func checkAccessLevel(c context.Context, r *http.Request, level AccessLevel) error { 38 if accessLevel(c, r) >= level { 39 return nil 40 } 41 if u := user.Current(c); u != nil { 42 // Log only if user is signed in. Not-signed-in users are redirected to login page. 43 log.Errorf(c, "unauthorized access: %q [%q] access level %v, url %.100s", 44 u.Email, u.AuthDomain, level, getCurrentURL(c)) 45 } 46 return ErrAccess 47 } 48 49 // AuthDomain is broken in AppEngine tests. 50 var isBrokenAuthDomainInTest = false 51 52 func accessLevel(c context.Context, r *http.Request) AccessLevel { 53 if user.IsAdmin(c) { 54 switch r.FormValue("access") { 55 case "public": 56 return AccessPublic 57 case "user": 58 return AccessUser 59 } 60 return AccessAdmin 61 } 62 u := user.Current(c) 63 if u == nil || 64 // Devappserver does not pass AuthDomain. 65 u.AuthDomain != "gmail.com" && !isBrokenAuthDomainInTest || 66 !strings.HasSuffix(u.Email, getConfig(c).AuthDomain) { 67 return AccessPublic 68 } 69 return AccessUser 70 } 71 72 func checkTextAccess(c context.Context, r *http.Request, tag string, id int64) (*Bug, *Crash, error) { 73 switch tag { 74 default: 75 return nil, nil, checkAccessLevel(c, r, AccessAdmin) 76 case textPatch: 77 return nil, nil, checkJobTextAccess(c, r, "Patch", id) 78 case textLog: 79 return nil, nil, checkJobTextAccess(c, r, "Log", id) 80 case textError: 81 return nil, nil, checkJobTextAccess(c, r, "Error", id) 82 case textKernelConfig: 83 // This is checked based on text namespace. 84 return nil, nil, nil 85 case textCrashLog: 86 // Log and Report can be attached to a Crash or Job. 87 bug, crash, err := checkCrashTextAccess(c, r, "Log", id) 88 if err == nil || err == ErrAccess { 89 return bug, crash, err 90 } 91 return nil, nil, checkJobTextAccess(c, r, "CrashLog", id) 92 case textCrashReport: 93 bug, crash, err := checkCrashTextAccess(c, r, "Report", id) 94 if err == nil || err == ErrAccess { 95 return bug, crash, err 96 } 97 return nil, nil, checkJobTextAccess(c, r, "CrashReport", id) 98 case textReproSyz: 99 return checkCrashTextAccess(c, r, "ReproSyz", id) 100 case textReproC: 101 return checkCrashTextAccess(c, r, "ReproC", id) 102 case textMachineInfo: 103 // MachineInfo is deduplicated, so we can't find the exact crash/bug. 104 // But since machine info is usually the same for all bugs and is not secret, 105 // it's fine to check based on the namespace. 106 return nil, nil, nil 107 } 108 } 109 110 func checkCrashTextAccess(c context.Context, r *http.Request, field string, id int64) (*Bug, *Crash, error) { 111 var crashes []*Crash 112 keys, err := db.NewQuery("Crash"). 113 Filter(field+"=", id). 114 GetAll(c, &crashes) 115 if err != nil { 116 return nil, nil, fmt.Errorf("failed to query crashes: %w", err) 117 } 118 if len(crashes) != 1 { 119 err := fmt.Errorf("checkCrashTextAccess: found %v crashes for %v=%v", len(crashes), field, id) 120 if len(crashes) == 0 { 121 err = fmt.Errorf("%w: %w", ErrClientNotFound, err) 122 } 123 return nil, nil, err 124 } 125 crash := crashes[0] 126 bug := new(Bug) 127 if err := db.Get(c, keys[0].Parent(), bug); err != nil { 128 return nil, nil, fmt.Errorf("failed to get bug: %w", err) 129 } 130 bugLevel := bug.sanitizeAccess(c, accessLevel(c, r)) 131 return bug, crash, checkAccessLevel(c, r, bugLevel) 132 } 133 134 func checkJobTextAccess(c context.Context, r *http.Request, field string, id int64) error { 135 keys, err := db.NewQuery("Job"). 136 Filter(field+"=", id). 137 KeysOnly(). 138 GetAll(c, nil) 139 if err != nil { 140 return fmt.Errorf("failed to query jobs: %w", err) 141 } 142 if len(keys) != 1 { 143 err := fmt.Errorf("checkJobTextAccess: found %v jobs for %v=%v", len(keys), field, id) 144 if len(keys) == 0 { 145 // This can be triggered by bad user requests, so don't log the error. 146 err = fmt.Errorf("%w: %w", ErrClientNotFound, err) 147 } 148 return err 149 } 150 bug := new(Bug) 151 if err := db.Get(c, keys[0].Parent(), bug); err != nil { 152 return fmt.Errorf("failed to get bug: %w", err) 153 } 154 bugLevel := bug.sanitizeAccess(c, accessLevel(c, r)) 155 return checkAccessLevel(c, r, bugLevel) 156 } 157 158 func (bug *Bug) sanitizeAccess(c context.Context, currentLevel AccessLevel) AccessLevel { 159 config := getConfig(c) 160 for ri := len(bug.Reporting) - 1; ri >= 0; ri-- { 161 bugReporting := &bug.Reporting[ri] 162 if ri == 0 || !bugReporting.Reported.IsZero() { 163 ns := config.Namespaces[bug.Namespace] 164 bugLevel := ns.ReportingByName(bugReporting.Name).AccessLevel 165 if currentLevel < bugLevel { 166 if bug.Status == BugStatusInvalid || 167 bug.Status == BugStatusFixed || len(bug.Commits) != 0 { 168 // Invalid and fixed bugs are visible in all reportings, 169 // however, without previous reporting private information. 170 lastLevel := ns.Reporting[len(ns.Reporting)-1].AccessLevel 171 if currentLevel >= lastLevel { 172 bugLevel = lastLevel 173 sanitizeReporting(bug) 174 } 175 } 176 } 177 return bugLevel 178 } 179 } 180 panic("unreachable") 181 } 182 183 func sanitizeReporting(bug *Bug) { 184 bug.DupOf = "" 185 for ri := range bug.Reporting { 186 bugReporting := &bug.Reporting[ri] 187 bugReporting.ID = "" 188 bugReporting.ExtID = "" 189 bugReporting.Link = "" 190 } 191 }