github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/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 func isEmailAuthorized(email string, acls []*ACLItem) (bool, AccessLevel) { 50 for _, acl := range acls { 51 if acl.Domain != "" && strings.HasSuffix(email, "@"+acl.Domain) || 52 acl.Email != "" && email == acl.Email { 53 return true, acl.Access 54 } 55 } 56 return false, AccessPublic 57 } 58 59 func currentUser(c context.Context) *user.User { 60 u := user.Current(c) 61 if u != nil { 62 return u 63 } 64 // Let's ignore err here. In case of the wrong token we'll return nil here (it means AccessPublic). 65 // Bad or expired tokens will also enable throttling and make the authorization problem visible. 66 u, _ = user.CurrentOAuth(c, "https://www.googleapis.com/auth/userinfo.email") 67 return u 68 } 69 70 // accessLevel supports 2 authorization mechanisms. 71 // They're checked in the following order: 72 // 1. AppEngine authorization. To authenticate yourself, click "Sign-in" on the dashboard page. 73 // 2. OAuth2 bearer token generated by "gcloud auth print-access-token" call. 74 // 75 // OAuth2 token is expected to be present in "Authorization" header. 76 // Example: "Authorization: Bearer $(gcloud auth print-access-token)". 77 func accessLevel(c context.Context, r *http.Request) AccessLevel { 78 _, al := userAccessLevel(currentUser(c), r.FormValue("access"), getConfig(c)) 79 return al 80 } 81 82 // trustedAuthDomain for the test environment is "". 83 var trustedAuthDomain = "gmail.com" 84 85 // userAccessLevel returns authorization flag and AccessLevel. 86 // (True, AccessAdmin) means authorized, Admin access. 87 // Note - authorize higher levels first. 88 func userAccessLevel(u *user.User, wantAccess string, config *GlobalConfig) (bool, AccessLevel) { 89 if u == nil || u.AuthDomain != trustedAuthDomain { 90 return false, AccessPublic 91 } 92 if u.Admin { 93 switch wantAccess { 94 case "public": 95 return true, AccessPublic 96 case "user": 97 return true, AccessUser 98 } 99 return true, AccessAdmin 100 } 101 return isEmailAuthorized(u.Email, config.ACL) 102 } 103 104 func checkTextAccess(c context.Context, r *http.Request, tag string, id int64) (*Bug, *Crash, error) { 105 switch tag { 106 default: 107 return nil, nil, checkAccessLevel(c, r, AccessAdmin) 108 case textPatch: 109 return nil, nil, checkJobTextAccess(c, r, "Patch", id) 110 case textLog: 111 return nil, nil, checkJobTextAccess(c, r, "Log", id) 112 case textError: 113 return nil, nil, checkJobTextAccess(c, r, "Error", id) 114 case textKernelConfig: 115 // This is checked based on text namespace. 116 return nil, nil, nil 117 case textCrashLog: 118 // Log and Report can be attached to a Crash or Job. 119 bug, crash, err := checkCrashTextAccess(c, r, "Log", id) 120 if err == nil || err == ErrAccess { 121 return bug, crash, err 122 } 123 return nil, nil, checkJobTextAccess(c, r, "CrashLog", id) 124 case textCrashReport: 125 bug, crash, err := checkCrashTextAccess(c, r, "Report", id) 126 if err == nil || err == ErrAccess { 127 return bug, crash, err 128 } 129 return nil, nil, checkJobTextAccess(c, r, "CrashReport", id) 130 case textReproSyz: 131 return checkCrashTextAccess(c, r, "ReproSyz", id) 132 case textReproC: 133 return checkCrashTextAccess(c, r, "ReproC", id) 134 case textReproLog: 135 bug, crash, err := checkCrashTextAccess(c, r, "ReproLog", id) 136 if err == nil || err == ErrAccess { 137 return bug, crash, err 138 } 139 // ReproLog might also be referenced from Bug.ReproAttempts.Log 140 // for failed repro attempts, but those are not exposed to non-admins 141 // as of yet, so fallback to normal admin access check. 142 return nil, nil, checkAccessLevel(c, r, AccessAdmin) 143 case textMachineInfo: 144 // MachineInfo is deduplicated, so we can't find the exact crash/bug. 145 // But since machine info is usually the same for all bugs and is not secret, 146 // it's fine to check based on the namespace. 147 return nil, nil, nil 148 case textFsckLog: 149 return checkCrashTextAccess(c, r, "Assets.FsckLog", id) 150 } 151 } 152 153 func checkCrashTextAccess(c context.Context, r *http.Request, field string, id int64) (*Bug, *Crash, error) { 154 var crashes []*Crash 155 keys, err := db.NewQuery("Crash"). 156 Filter(field+"=", id). 157 GetAll(c, &crashes) 158 if err != nil { 159 return nil, nil, fmt.Errorf("failed to query crashes: %w", err) 160 } 161 if len(crashes) != 1 { 162 err := fmt.Errorf("checkCrashTextAccess: found %v crashes for %v=%v", len(crashes), field, id) 163 if len(crashes) == 0 { 164 err = fmt.Errorf("%w: %w", ErrClientNotFound, err) 165 } 166 return nil, nil, err 167 } 168 crash := crashes[0] 169 bug := new(Bug) 170 if err := db.Get(c, keys[0].Parent(), bug); err != nil { 171 return nil, nil, fmt.Errorf("failed to get bug: %w", err) 172 } 173 bugLevel := bug.sanitizeAccess(c, accessLevel(c, r)) 174 return bug, crash, checkAccessLevel(c, r, bugLevel) 175 } 176 177 func checkJobTextAccess(c context.Context, r *http.Request, field string, id int64) error { 178 keys, err := db.NewQuery("Job"). 179 Filter(field+"=", id). 180 KeysOnly(). 181 GetAll(c, nil) 182 if err != nil { 183 return fmt.Errorf("failed to query jobs: %w", err) 184 } 185 if len(keys) != 1 { 186 err := fmt.Errorf("checkJobTextAccess: found %v jobs for %v=%v", len(keys), field, id) 187 if len(keys) == 0 { 188 // This can be triggered by bad user requests, so don't log the error. 189 err = fmt.Errorf("%w: %w", ErrClientNotFound, err) 190 } 191 return err 192 } 193 bug := new(Bug) 194 if err := db.Get(c, keys[0].Parent(), bug); err != nil { 195 return fmt.Errorf("failed to get bug: %w", err) 196 } 197 bugLevel := bug.sanitizeAccess(c, accessLevel(c, r)) 198 return checkAccessLevel(c, r, bugLevel) 199 } 200 201 func (bug *Bug) sanitizeAccess(c context.Context, currentLevel AccessLevel) AccessLevel { 202 config := getConfig(c) 203 for ri := len(bug.Reporting) - 1; ri >= 0; ri-- { 204 bugReporting := &bug.Reporting[ri] 205 if ri == 0 || !bugReporting.Reported.IsZero() { 206 ns := config.Namespaces[bug.Namespace] 207 bugLevel := ns.ReportingByName(bugReporting.Name).AccessLevel 208 if currentLevel < bugLevel { 209 if bug.Status == BugStatusInvalid || 210 bug.Status == BugStatusFixed || len(bug.Commits) != 0 { 211 // Invalid and fixed bugs are visible in all reportings, 212 // however, without previous reporting private information. 213 lastLevel := ns.Reporting[len(ns.Reporting)-1].AccessLevel 214 if currentLevel >= lastLevel { 215 bugLevel = lastLevel 216 sanitizeReporting(bug) 217 } 218 } 219 } 220 return bugLevel 221 } 222 } 223 panic("unreachable") 224 } 225 226 func sanitizeReporting(bug *Bug) { 227 bug.DupOf = "" 228 for ri := range bug.Reporting { 229 bugReporting := &bug.Reporting[ri] 230 bugReporting.ID = "" 231 bugReporting.ExtID = "" 232 bugReporting.Link = "" 233 } 234 }