storage: require OAuth to upload benchmark results

Change-Id: Ia48bc4e1bb918b4d2e28752cfe45f31ac9955e47
Reviewed-on: https://go-review.googlesource.com/35055
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/storage/app/app.go b/storage/app/app.go
index 08f97af..fea66b0 100644
--- a/storage/app/app.go
+++ b/storage/app/app.go
@@ -7,6 +7,7 @@
 package app
 
 import (
+	"errors"
 	"net/http"
 
 	"golang.org/x/perf/storage/db"
@@ -19,8 +20,16 @@
 type App struct {
 	DB *db.DB
 	FS fs.FS
+
+	// Auth obtains the username for the request.
+	// If necessary, it can write its own response (e.g. a
+	// redirect) and return ErrResponseWritten.
+	Auth func(http.ResponseWriter, *http.Request) (string, error)
 }
 
+// ErrResponseWritten can be returned by App.Auth to abort the normal /upload handling.
+var ErrResponseWritten = errors.New("response written")
+
 // RegisterOnMux registers the app's URLs on mux.
 func (a *App) RegisterOnMux(mux *http.ServeMux) {
 	// TODO(quentin): Should we just make the App itself be an http.Handler?
diff --git a/storage/app/query_test.go b/storage/app/query_test.go
index 7c79e94..ac689a9 100644
--- a/storage/app/query_test.go
+++ b/storage/app/query_test.go
@@ -75,6 +75,9 @@
 				if r.NameLabels["name"] != "Name" {
 					t.Errorf("#%d: name = %q, want %q", i, r.NameLabels["name"], "Name")
 				}
+				if r.Labels["by"] != "user" {
+					t.Errorf("#%d: by = %q, want %q", i, r.Labels["uploader"], "user")
+				}
 			}
 			_, err = br.Next()
 			if err != io.EOF {
diff --git a/storage/app/upload.go b/storage/app/upload.go
index d556839..60467df 100644
--- a/storage/app/upload.go
+++ b/storage/app/upload.go
@@ -24,7 +24,13 @@
 func (a *App) upload(w http.ResponseWriter, r *http.Request) {
 	ctx := requestContext(r)
 
-	// TODO(quentin): Authentication
+	user, err := a.Auth(w, r)
+	switch {
+	case err == ErrResponseWritten:
+		return
+	case err != nil:
+		http.Error(w, err.Error(), 500)
+	}
 
 	if r.Method == http.MethodGet {
 		http.ServeFile(w, r, "static/upload.html")
@@ -44,7 +50,7 @@
 		return
 	}
 
-	result, err := a.processUpload(ctx, mr)
+	result, err := a.processUpload(ctx, user, mr)
 	if err != nil {
 		errorf(ctx, "%v", err)
 		http.Error(w, err.Error(), 500)
@@ -69,7 +75,7 @@
 
 // processUpload takes one or more files from a multipart.Reader,
 // writes them to the filesystem, and indexes their content.
-func (a *App) processUpload(ctx context.Context, mr *multipart.Reader) (*uploadStatus, error) {
+func (a *App) processUpload(ctx context.Context, user string, mr *multipart.Reader) (*uploadStatus, error) {
 	var upload *db.Upload
 	var fileids []string
 
@@ -97,7 +103,7 @@
 		// is invalid (contains no valid records) it needs to
 		// be rejected and the Cloud Storage upload aborted.
 
-		meta := fileMetadata(ctx, upload.ID, i)
+		meta := fileMetadata(user, upload.ID, i)
 
 		// We need to do two things with the incoming data:
 		// - Write it to permanent storage via a.FS
@@ -162,14 +168,16 @@
 }
 
 // fileMetadata returns the extra metadata fields associated with an
-// uploaded file. It obtains the uploader's e-mail address from the
-// Context.
-func fileMetadata(_ context.Context, uploadid string, filenum int) map[string]string {
-	// TODO(quentin): Add the name of the uploader.
+// uploaded file.
+func fileMetadata(user string, uploadid string, filenum int) map[string]string {
 	// TODO(quentin): Add the upload time.
 	// TODO(quentin): Add other fields?
-	return map[string]string{
+	m := map[string]string{
 		"uploadid": uploadid,
 		"fileid":   fmt.Sprintf("%s/%d", uploadid, filenum),
 	}
+	if user != "" {
+		m["by"] = user
+	}
+	return m
 }
diff --git a/storage/app/upload_test.go b/storage/app/upload_test.go
index 90581cc..7f976e0 100644
--- a/storage/app/upload_test.go
+++ b/storage/app/upload_test.go
@@ -43,7 +43,11 @@
 
 	fs := fs.NewMemFS()
 
-	app := &App{DB: db, FS: fs}
+	app := &App{
+		DB:   db,
+		FS:   fs,
+		Auth: func(http.ResponseWriter, *http.Request) (string, error) { return "user", nil },
+	}
 
 	mux := http.NewServeMux()
 	app.RegisterOnMux(mux)
diff --git a/storage/appengine/app.go b/storage/appengine/app.go
index a3b3007..498e812 100644
--- a/storage/appengine/app.go
+++ b/storage/appengine/app.go
@@ -17,6 +17,7 @@
 	"golang.org/x/perf/storage/fs/gcs"
 	"google.golang.org/appengine"
 	aelog "google.golang.org/appengine/log"
+	"google.golang.org/appengine/user"
 )
 
 // connectDB returns a DB initialized from the environment variables set in app.yaml. CLOUDSQL_CONNECTION_NAME, CLOUDSQL_USER, and CLOUDSQL_DATABASE must be set to point to the Cloud SQL instance. CLOUDSQL_PASSWORD can be set if needed.
@@ -39,6 +40,27 @@
 	return v
 }
 
+func auth(w http.ResponseWriter, r *http.Request) (string, error) {
+	ctx := appengine.NewContext(r)
+	u := user.Current(ctx)
+	if u == nil && r.Header.Get("Authorization") != "" {
+		var err error
+		u, err = user.CurrentOAuth(ctx, "https://www.googleapis.com/auth/userinfo.email")
+		if err != nil {
+			return "", err
+		}
+	}
+	if u == nil {
+		url, err := user.LoginURL(ctx, r.URL.String())
+		if err != nil {
+			return "", err
+		}
+		http.Redirect(w, r, url, http.StatusFound)
+		return "", app.ErrResponseWritten
+	}
+	return u.Email, nil
+}
+
 // appHandler is the default handler, registered to serve "/".
 // It creates a new App instance using the appengine Context and then
 // dispatches the request to the App. The environment variable
@@ -66,7 +88,7 @@
 		return
 	}
 	mux := http.NewServeMux()
-	app := &app.App{DB: db, FS: fs}
+	app := &app.App{DB: db, FS: fs, Auth: auth}
 	app.RegisterOnMux(mux)
 	mux.ServeHTTP(w, r)
 }
diff --git a/storage/localserver/app.go b/storage/localserver/app.go
index 4cfb9db..7fdfc97 100644
--- a/storage/localserver/app.go
+++ b/storage/localserver/app.go
@@ -26,7 +26,7 @@
 	}
 	fs := fs.NewMemFS()
 
-	app := &app.App{DB: db, FS: fs}
+	app := &app.App{DB: db, FS: fs, Auth: func(http.ResponseWriter, *http.Request) (string, error) { return "", nil }}
 	app.RegisterOnMux(http.DefaultServeMux)
 
 	log.Printf("Listening on %s", *host)