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  }