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  }