| // 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 dash |
| |
| import ( |
| "errors" |
| "fmt" |
| "net/http" |
| "strings" |
| |
| "golang.org/x/net/context" |
| "google.golang.org/appengine" |
| "google.golang.org/appengine/datastore" |
| "google.golang.org/appengine/log" |
| "google.golang.org/appengine/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", u.Email, u.AuthDomain, level) |
| } |
| return ErrAccess |
| } |
| |
| func accessLevel(c context.Context, r *http.Request) AccessLevel { |
| if user.IsAdmin(c) { |
| switch r.FormValue("access") { |
| case "public": |
| return AccessPublic |
| case "user": |
| return AccessUser |
| } |
| return AccessAdmin |
| } |
| u := user.Current(c) |
| if u == nil || |
| // devappserver is broken |
| u.AuthDomain != "gmail.com" && !appengine.IsDevAppServer() || |
| !strings.HasSuffix(u.Email, config.AuthDomain) { |
| return AccessPublic |
| } |
| return AccessUser |
| } |
| |
| func checkTextAccess(c context.Context, r *http.Request, tag string, id int64) (*Crash, error) { |
| switch tag { |
| default: |
| return nil, checkAccessLevel(c, r, AccessAdmin) |
| case textPatch: |
| return nil, checkJobTextAccess(c, r, "Patch", id) |
| case textError: |
| return nil, checkJobTextAccess(c, r, "Error", id) |
| case textKernelConfig: |
| // This is checked based on text namespace. |
| return nil, nil |
| case textCrashLog: |
| // Log and Report can be attached to a Crash or Job. |
| crash, err := checkCrashTextAccess(c, r, "Log", id) |
| if err == nil || err == ErrAccess { |
| return crash, err |
| } |
| return nil, checkJobTextAccess(c, r, "CrashLog", id) |
| case textCrashReport: |
| crash, err := checkCrashTextAccess(c, r, "Report", id) |
| if err == nil || err == ErrAccess { |
| return crash, err |
| } |
| return nil, checkJobTextAccess(c, r, "CrashReport", id) |
| case textReproSyz: |
| return checkCrashTextAccess(c, r, "ReproSyz", id) |
| case textReproC: |
| return checkCrashTextAccess(c, r, "ReproC", id) |
| } |
| } |
| |
| func checkCrashTextAccess(c context.Context, r *http.Request, field string, id int64) (*Crash, error) { |
| var crashes []*Crash |
| keys, err := datastore.NewQuery("Crash"). |
| Filter(field+"=", id). |
| GetAll(c, &crashes) |
| if err != nil { |
| return nil, fmt.Errorf("failed to query crashes: %v", err) |
| } |
| if len(crashes) != 1 { |
| return nil, fmt.Errorf("checkCrashTextAccess: found %v crashes for %v=%v", |
| len(crashes), field, id) |
| } |
| crash := crashes[0] |
| bug := new(Bug) |
| if err := datastore.Get(c, keys[0].Parent(), bug); err != nil { |
| return nil, fmt.Errorf("failed to get bug: %v", err) |
| } |
| bugLevel := bug.sanitizeAccess(accessLevel(c, r)) |
| return crash, checkAccessLevel(c, r, bugLevel) |
| } |
| |
| func checkJobTextAccess(c context.Context, r *http.Request, field string, id int64) error { |
| keys, err := datastore.NewQuery("Job"). |
| Filter(field+"=", id). |
| KeysOnly(). |
| GetAll(c, nil) |
| if err != nil { |
| return fmt.Errorf("failed to query jobs: %v", err) |
| } |
| if len(keys) != 1 { |
| return fmt.Errorf("checkJobTextAccess: found %v jobs for %v=%v", |
| len(keys), field, id) |
| } |
| bug := new(Bug) |
| if err := datastore.Get(c, keys[0].Parent(), bug); err != nil { |
| return fmt.Errorf("failed to get bug: %v", err) |
| } |
| bugLevel := bug.sanitizeAccess(accessLevel(c, r)) |
| return checkAccessLevel(c, r, bugLevel) |
| } |
| |
| func (bug *Bug) sanitizeAccess(currentLevel AccessLevel) AccessLevel { |
| 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 == BugStatusFixed || len(bug.Commits) != 0 { |
| // 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 = "" |
| } |
| } |