Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dashboard/app: introduce authorized access to public resources #5407

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions dashboard/app/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strings"

db "google.golang.org/appengine/v2/datastore"
Expand Down Expand Up @@ -46,20 +47,16 @@ func checkAccessLevel(c context.Context, r *http.Request, level AccessLevel) err
return ErrAccess
}

// AuthDomain is broken in AppEngine tests.
var isBrokenAuthDomainInTest = false

func emailInAuthDomains(email string, authDomains []string) bool {
for _, authDomain := range authDomains {
if strings.HasSuffix(email, authDomain) {
return true
}
}

return false
}

func currentUser(c context.Context, r *http.Request) *user.User {
func currentUser(c context.Context) *user.User {
u := user.Current(c)
if u != nil {
return u
Expand All @@ -78,23 +75,36 @@ func currentUser(c context.Context, r *http.Request) *user.User {
// 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 {
if user.IsAdmin(c) {
switch r.FormValue("access") {
al, _ := userAccessLevel(currentUser(c), r.FormValue("access"), getConfig(c))
return al
}

// trustedAuthDomain for the test environment is "".
var trustedAuthDomain = "gmail.com"

// userAccessLevel returns AccessLevel and throttling(speed) recommendation.
// (AccessAdmin, True) means Admin access, full speed.
// Note - authorize higher levels first.
func userAccessLevel(u *user.User, wantAccess string, config *GlobalConfig) (AccessLevel, bool) {
if u == nil || u.AuthDomain != trustedAuthDomain {
return AccessPublic, false
}
if u.Admin {
switch wantAccess {
case "public":
return AccessPublic
return AccessPublic, true
case "user":
return AccessUser
return AccessUser, true
}
return AccessAdmin
return AccessAdmin, true
}
if emailInAuthDomains(u.Email, config.AuthUserDomains) {
return AccessUser, true
}
u := currentUser(c, r)
if u == nil ||
// Devappserver does not pass AuthDomain.
u.AuthDomain != "gmail.com" && !isBrokenAuthDomainInTest ||
!emailInAuthDomains(u.Email, getConfig(c).AuthDomains) {
return AccessPublic
if slices.Contains(config.AuthPublicEmails, u.Email) {
return AccessPublic, true
}
return AccessUser
return AccessPublic, false
}

func checkTextAccess(c context.Context, r *http.Request, tag string, id int64) (*Bug, *Crash, error) {
Expand Down
108 changes: 108 additions & 0 deletions dashboard/app/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,111 @@ func TestAccess(t *testing.T) {
}
}
}

type UserAuthorizationLevel int

const (
BadAuthDomain UserAuthorizationLevel = iota
Regular
Authenticated
AuthorizedAccessPublic
AuthorizedUser
AuthorizedAdmin
)

func makeUser(a UserAuthorizationLevel) *user.User {
u := &user.User{
AuthDomain: "",
Admin: false,
FederatedIdentity: "",
FederatedProvider: "",
}
switch a {
case BadAuthDomain:
u.AuthDomain = "public.com"
case Regular:
u = nil
case Authenticated:
u.Email = "[email protected]"
case AuthorizedAccessPublic:
u.Email = "[email protected]"
case AuthorizedUser:
u.Email = "[email protected]"
case AuthorizedAdmin:
u.Email = "[email protected]"
u.Admin = true
}
return u
}

func TestUserAccessLevel(t *testing.T) {
tests := []struct {
name string
u *user.User
enforcedAccessLevel string
config *GlobalConfig
wantAccessLevel AccessLevel
wantFullSpeed bool
}{
{
name: "wrong auth domain",
u: makeUser(BadAuthDomain),
wantAccessLevel: AccessPublic,
},
{
name: "regular not authenticated user",
u: makeUser(Regular),
wantAccessLevel: AccessPublic,
},
{
name: "authenticated, not authorized user",
u: makeUser(Authenticated),
config: testConfig,
wantAccessLevel: AccessPublic,
},
{
name: "authorized for AccessPublic user",
u: makeUser(AuthorizedAccessPublic),
config: testConfig,
wantAccessLevel: AccessPublic,
wantFullSpeed: true,
},
{
name: "authorized for AccessUser user",
u: makeUser(AuthorizedUser),
config: testConfig,
wantAccessLevel: AccessUser,
wantFullSpeed: true,
},
{
name: "authorized admin wants AccessAdmin",
u: makeUser(AuthorizedAdmin),
config: testConfig,
wantAccessLevel: AccessAdmin,
wantFullSpeed: true,
},
{
name: "authorized admin wants AccessPublic",
u: makeUser(AuthorizedAdmin),
enforcedAccessLevel: "public",
config: testConfig,
wantAccessLevel: AccessPublic,
wantFullSpeed: true,
},
{
name: "authorized admin wants AccessUser",
u: makeUser(AuthorizedAdmin),
enforcedAccessLevel: "user",
config: testConfig,
wantAccessLevel: AccessUser,
wantFullSpeed: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotAccessLevel, gotFullSpeed := userAccessLevel(test.u, test.enforcedAccessLevel, test.config)
assert.Equal(t, test.wantAccessLevel, gotAccessLevel)
assert.Equal(t, test.wantFullSpeed, gotFullSpeed)
})
}
}
7 changes: 4 additions & 3 deletions dashboard/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func init() {
os.Setenv("GAE_MODULE_VERSION", "1")
os.Setenv("GAE_MINOR_VERSION", "1")

isBrokenAuthDomainInTest = true
trustedAuthDomain = "" // Devappserver environment value is "", prod value is "gmail.com".
obsoleteWhatWontBeFixBisected = true
notifyAboutUnsuccessfulBisections = true
ensureConfigImmutability = true
Expand All @@ -41,8 +41,9 @@ func init() {

// Config used in tests.
var testConfig = &GlobalConfig{
AccessLevel: AccessPublic,
AuthDomains: []string{"@syzkaller.com"},
AccessLevel: AccessPublic,
AuthUserDomains: []string{"@syzkaller.com"},
AuthPublicEmails: []string{makeUser(AuthorizedAccessPublic).Email},
Clients: map[string]string{
"reporting": "reportingkeyreportingkeyreportingkey",
},
Expand Down
2 changes: 1 addition & 1 deletion dashboard/app/bisect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,7 @@ func TestBugBisectionInvalidation(t *testing.T) {
c.expectEQ(err, nil)
c.expectTrue(!bytes.Contains(content, []byte("Cause bisection: introduced by")))
c.expectTrue(!bytes.Contains(content, []byte("kernel: add a bug")))
c.expectEQ(job.InvalidatedBy, "[email protected]")
c.expectEQ(job.InvalidatedBy, makeUser(AuthorizedAdmin).Email)

// Wait 30 days, no new cause bisection jobs should be created.
c.advanceTime(24 * 30 * time.Hour)
Expand Down
17 changes: 10 additions & 7 deletions dashboard/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import (
type GlobalConfig struct {
// Min access levels specified hierarchically throughout the config.
AccessLevel AccessLevel
// Email suffixes of authorized users (e.g. []string{"@foo.com","@bar.org"}).
AuthDomains []string
// AuthUserDomains is a list of email prefixes authorized for UserAccess (e.g. []string{"@foo.com","@bar.org"}).
AuthUserDomains []string
// AuthPublicEmails is a list of emails authorized for PublicAccess w/o throttling.
AuthPublicEmails []string
// Google Analytics Tracking ID.
AnalyticsTrackingID string
// URL prefix of source coverage reports.
Expand Down Expand Up @@ -455,13 +457,14 @@ func checkConfig(cfg *GlobalConfig) {
checkNamespace(ns, cfg, namespaces, clientNames)
}
checkDiscussionEmails(cfg.DiscussionEmails)
checkAuthDomains(cfg.AuthDomains)
checkDomainSeparator(cfg.AuthUserDomains)
checkDomainSeparator(cfg.AuthPublicEmails)
}

func checkAuthDomains(list []string) {
for _, domain := range list {
if !strings.HasPrefix(domain, "@") {
panic(fmt.Sprintf("authentication domain %s doesn't start with @", domain))
func checkDomainSeparator(list []string) {
for _, target := range list {
if strings.Count(target, "@") != 1 {
panic(fmt.Sprintf("authorization for %s isn't possible, need @", target))
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion dashboard/app/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ func handleContext(fn contextHandler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
c = context.WithValue(c, &currentURLKey, r.URL.RequestURI())
if accessLevel(c, r) == AccessPublic {
_, fullSpeed := userAccessLevel(currentUser(c), "", getConfig(c))
if !fullSpeed {
if !throttleRequest(c, w, r) {
return
}
Expand Down
14 changes: 4 additions & 10 deletions dashboard/app/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import (
db "google.golang.org/appengine/v2/datastore"
"google.golang.org/appengine/v2/log"
aemail "google.golang.org/appengine/v2/mail"
"google.golang.org/appengine/v2/user"
)

type Ctx struct {
Expand Down Expand Up @@ -341,15 +340,10 @@ func (c *Ctx) httpRequest(method, url, body, contentType string,
}
r = registerRequest(r, c)
r = r.WithContext(c.transformContext(r.Context()))
if access == AccessAdmin || access == AccessUser {
user := &user.User{
Email: "[email protected]",
AuthDomain: "gmail.com",
}
if access == AccessAdmin {
user.Admin = true
}
aetest.Login(user, r)
if access == AccessAdmin {
aetest.Login(makeUser(AuthorizedAdmin), r)
} else if access == AccessUser {
aetest.Login(makeUser(AuthorizedUser), r)
}
w := httptest.NewRecorder()
http.DefaultServeMux.ServeHTTP(w, r)
Expand Down
Loading