1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
|
// 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"
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
}
// trustedAuthDomain for the test environment is "".
var trustedAuthDomain = "gmail.com"
// 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) {
if u == nil || u.AuthDomain != trustedAuthDomain {
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 = ""
}
}
|