// 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. package main import ( "context" "errors" "fmt" "net/http" "strings" "google.golang.org/appengine/v2" db "google.golang.org/appengine/v2/datastore" "google.golang.org/appengine/v2/log" "google.golang.org/appengine/v2/user" ) type AccessLevel int const ( AccessPublic AccessLevel = iota + 1 AccessUser AccessAdmin ) func verifyAccessLevel(access AccessLevel) { switch access { case AccessPublic, AccessUser, AccessAdmin: return default: panic(fmt.Sprintf("bad access level %v", access)) } } var ErrAccess = errors.New("unauthorized") func checkAccessLevel(c context.Context, r *http.Request, level AccessLevel) error { if accessLevel(c, r) >= level { return nil } if u := user.Current(c); u != nil { // Log only if user is signed in. Not-signed-in users are redirected to login page. log.Errorf(c, "unauthorized access: %q [%q] access level %v, url %.100s", u.Email, u.AuthDomain, level, getCurrentURL(c)) } return ErrAccess } func isEmailAuthorized(email string, acls []*ACLItem) (bool, AccessLevel) { for _, acl := range acls { if acl.Domain != "" && strings.HasSuffix(email, "@"+acl.Domain) || acl.Email != "" && email == acl.Email { return true, acl.Access } } return false, AccessPublic } func currentUser(c context.Context) *user.User { u := user.Current(c) if u != nil { return u } // Let's ignore err here. In case of the wrong token we'll return nil here (it means AccessPublic). // Bad or expired tokens will also enable throttling and make the authorization problem visible. u, _ = user.CurrentOAuth(c, "https://www.googleapis.com/auth/userinfo.email") return u } // accessLevel supports 2 authorization mechanisms. // They're checked in the following order: // 1. AppEngine authorization. To authenticate yourself, click "Sign-in" on the dashboard page. // 2. OAuth2 bearer token generated by "gcloud auth print-access-token" call. // // OAuth2 token is expected to be present in "Authorization" header. // Example: "Authorization: Bearer $(gcloud auth print-access-token)". func accessLevel(c context.Context, r *http.Request) AccessLevel { _, al := userAccessLevel(currentUser(c), r.FormValue("access"), getConfig(c)) return al } const prodAuthDomain = "gmail.com" // trustedAuthDomain for the test environment is "". var trustedAuthDomain = prodAuthDomain // userAccessLevel returns authorization flag and AccessLevel. // (True, AccessAdmin) means authorized, Admin access. // Note - authorize higher levels first. func userAccessLevel(u *user.User, wantAccess string, config *GlobalConfig) (bool, AccessLevel) { domainOK := u != nil && (u.AuthDomain == trustedAuthDomain || // This supports local runs of dev_appserver.py where trustedAuthDomain // is not overridden, but dev_appserver.py sets u.AuthDomain="". appengine.IsDevAppServer() && trustedAuthDomain == prodAuthDomain) if !domainOK { return false, AccessPublic } if u.Admin { switch wantAccess { case "public": return true, AccessPublic case "user": return true, AccessUser } return true, AccessAdmin } return isEmailAuthorized(u.Email, config.ACL) } func checkTextAccess(c context.Context, r *http.Request, tag string, id int64) (*Bug, *Crash, error) { switch tag { default: return nil, nil, checkAccessLevel(c, r, AccessAdmin) case textPatch: return nil, nil, checkJobTextAccess(c, r, "Patch", id) case textLog: return nil, nil, checkJobTextAccess(c, r, "Log", id) case textError: return nil, nil, checkJobTextAccess(c, r, "Error", id) case textKernelConfig: // This is checked based on text namespace. return nil, nil, nil case textCrashLog: // Log and Report can be attached to a Crash or Job. bug, crash, err := checkCrashTextAccess(c, r, "Log", id) if err == nil || err == ErrAccess { return bug, crash, err } return nil, nil, checkJobTextAccess(c, r, "CrashLog", id) case textCrashReport: bug, crash, err := checkCrashTextAccess(c, r, "Report", id) if err == nil || err == ErrAccess { return bug, crash, err } return nil, nil, checkJobTextAccess(c, r, "CrashReport", id) case textReproSyz: return checkCrashTextAccess(c, r, "ReproSyz", id) case textReproC: return checkCrashTextAccess(c, r, "ReproC", id) case textReproLog: bug, crash, err := checkCrashTextAccess(c, r, "ReproLog", id) if err == nil || err == ErrAccess { return bug, crash, err } // ReproLog might also be referenced from Bug.ReproAttempts.Log // for failed repro attempts, but those are not exposed to non-admins // as of yet, so fallback to normal admin access check. return nil, nil, checkAccessLevel(c, r, AccessAdmin) case textMachineInfo: // MachineInfo is deduplicated, so we can't find the exact crash/bug. // But since machine info is usually the same for all bugs and is not secret, // it's fine to check based on the namespace. return nil, nil, nil case textFsckLog: return checkCrashTextAccess(c, r, "Assets.FsckLog", id) } } func checkCrashTextAccess(c context.Context, r *http.Request, field string, id int64) (*Bug, *Crash, error) { var crashes []*Crash keys, err := db.NewQuery("Crash"). Filter(field+"=", id). GetAll(c, &crashes) if err != nil { return nil, nil, fmt.Errorf("failed to query crashes: %w", err) } if len(crashes) != 1 { err := fmt.Errorf("checkCrashTextAccess: found %v crashes for %v=%v", len(crashes), field, id) if len(crashes) == 0 { err = fmt.Errorf("%w: %w", ErrClientNotFound, err) } return nil, nil, err } crash := crashes[0] bug := new(Bug) if err := db.Get(c, keys[0].Parent(), bug); err != nil { return nil, nil, fmt.Errorf("failed to get bug: %w", err) } bugLevel := bug.sanitizeAccess(c, accessLevel(c, r)) return bug, crash, checkAccessLevel(c, r, bugLevel) } func checkJobTextAccess(c context.Context, r *http.Request, field string, id int64) error { keys, err := db.NewQuery("Job"). Filter(field+"=", id). KeysOnly(). GetAll(c, nil) if err != nil { return fmt.Errorf("failed to query jobs: %w", err) } if len(keys) != 1 { err := fmt.Errorf("checkJobTextAccess: found %v jobs for %v=%v", len(keys), field, id) if len(keys) == 0 { // This can be triggered by bad user requests, so don't log the error. err = fmt.Errorf("%w: %w", ErrClientNotFound, err) } return err } bug := new(Bug) if err := db.Get(c, keys[0].Parent(), bug); err != nil { return fmt.Errorf("failed to get bug: %w", err) } bugLevel := bug.sanitizeAccess(c, accessLevel(c, r)) return checkAccessLevel(c, r, bugLevel) } func (bug *Bug) sanitizeAccess(c context.Context, currentLevel AccessLevel) AccessLevel { config := getConfig(c) for ri := len(bug.Reporting) - 1; ri >= 0; ri-- { bugReporting := &bug.Reporting[ri] if ri == 0 || !bugReporting.Reported.IsZero() { ns := config.Namespaces[bug.Namespace] bugLevel := ns.ReportingByName(bugReporting.Name).AccessLevel if currentLevel < bugLevel { if bug.Status == BugStatusInvalid || bug.Status == BugStatusFixed || len(bug.Commits) != 0 { // Invalid and fixed bugs are visible in all reportings, // however, without previous reporting private information. lastLevel := ns.Reporting[len(ns.Reporting)-1].AccessLevel if currentLevel >= lastLevel { bugLevel = lastLevel sanitizeReporting(bug) } } } return bugLevel } } panic("unreachable") } func sanitizeReporting(bug *Bug) { bug.DupOf = "" for ri := range bug.Reporting { bugReporting := &bug.Reporting[ri] bugReporting.ID = "" bugReporting.ExtID = "" bugReporting.Link = "" } }