Skip to content
Merged
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
64 changes: 54 additions & 10 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"google.golang.org/appengine"

"google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
"google.golang.org/appengine/user"
)
Expand All @@ -37,6 +38,11 @@ func init() {
// IMPORTANT: This is the ONLY path that does NOT require authentication. Do
// not copy this pattern. Use registerRPC instead.
http.HandleFunc("/api/get-authentication-status", getAuthenticationStatus)

// IMPORTANT: Do not expose the handlers below in production.
if appengine.IsDevAppServer() {
http.HandleFunc("/api/whitelist-account", whitelistAccount)
}
}

func registerRPC(path string, handler func(cocoon *db.Cocoon, inputJSON []byte) (interface{}, error)) {
Expand Down Expand Up @@ -120,7 +126,12 @@ func getAuthenticatedContext(ctx context.Context, r *http.Request) (context.Cont
}

if !strings.HasSuffix(user.Email, "@google.com") {
return nil, fmt.Errorf("Only @google.com users are authorized")
cocoon := db.NewCocoon(ctx)
err := cocoon.IsWhitelisted(user.Email)

if err != nil {
return nil, err
}
}

return ctx, nil
Expand All @@ -134,19 +145,52 @@ func getAuthenticationStatus(w http.ResponseWriter, r *http.Request) {
// errors.
_, err := getAuthenticatedContext(ctx, r)

var response map[string]interface{}

var status string
if err == nil {
response = map[string]interface{}{
"Status": "OK",
}
status = "OK"
} else {
loginURL, _ := user.LoginURL(ctx, "/")
response = map[string]interface{}{
"LoginURL": loginURL,
}
status = "Unauthorized"
}

loginURL, _ := user.LoginURL(ctx, "/build.html")
logoutURL, _ := user.LogoutURL(ctx, "/build.html")

response := map[string]interface{}{
"Status": status,
"LoginURL": loginURL,
"LogoutURL": logoutURL,
}

outputData, _ := json.Marshal(response)
w.Write(outputData)
}

// Adds the provided email address to the authorized Google account whitelist.
//
// Available only on the local dev server.
func whitelistAccount(w http.ResponseWriter, r *http.Request) {
if !appengine.IsDevAppServer() {
panic("whitelistAccount is only available on the local dev server")
}

ctx := appengine.NewContext(r)
email := strings.TrimSpace(r.URL.Query().Get("email"))

if len(email) == 0 {
serveError(ctx, w, r, fmt.Errorf("Bad email address: %v", email))
return
}

account := &db.WhitelistedAccount{
Email: email,
}

key := datastore.NewIncompleteKey(ctx, "WhitelistedAccount", nil)
_, err := datastore.Put(ctx, key, account)
if err != nil {
serveError(ctx, w, r, err)
return
}

w.Write([]byte("OK"))
}
5 changes: 5 additions & 0 deletions app/web/build.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
<status-table></status-table>
</div>

<footer>
<button id="logout-button">Sign out of Google</button>
<button id="login-button">Sign in with Google</button>
</footer>

<script defer src="main.dart" type="application/dart"></script>
<script defer src="packages/browser/dart.js"></script>
</body>
Expand Down
4 changes: 2 additions & 2 deletions app/web/buildStyles.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ footer {
bottom: 0;
left: 0;
right: 0;
padding: 2px 19px 10px 19px;
padding: 2px 19px;
z-index: 2;
background: inherit;
background-color: #917FFF;
display: flex;
}

Expand Down
2 changes: 2 additions & 0 deletions app/web/buildbot.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use strict";

// Globally visible list of current build statuses encoded as:
//
// {
Expand Down
2 changes: 2 additions & 0 deletions app/web/firebase.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use strict";

var ref = new Firebase("https://purple-butterfly-3000.firebaseio.com/");

var whenFirebaseReady = new Promise(function(resolve, reject) {
Expand Down
4 changes: 3 additions & 1 deletion app/web/github.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use strict";

(function() {

var ACCESS_TOKEN = '6a93c73f10808b9a92d4ec8a91b9e823c192d204';
Expand Down Expand Up @@ -36,4 +38,4 @@
}

displayMilestone();
})();
})();
19 changes: 16 additions & 3 deletions app/web/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ main() async {
logger = new HtmlLogger();
http.Client httpClient = await _getAuthenticatedClientOrRedirectToSignIn();

if (httpClient == null)
return;

// Start the angular app
ComponentRef ref = await bootstrap(StatusTable, [
provide(http.Client, useValue: httpClient),
Expand All @@ -33,12 +36,22 @@ Future<http.Client> _getAuthenticatedClientOrRedirectToSignIn() async {
http.Client client = new browser_http.BrowserClient();
Map<String, dynamic> status = JSON.decode((await client.get('/api/get-authentication-status')).body);

document.querySelector('#logout-button').on['click'].listen((_) {
window.open(status['LogoutURL'], '_self');
});

document.querySelector('#login-button').on['click'].listen((_) {
window.open(status['LoginURL'], '_self');
});

if (status['Status'] == 'OK') {
return client;
} else {
window.open(status['LoginURL'], '_self');
return null;
}

document.body.append(new DivElement()
..text = 'You are not signed in, or signed in under an unauthorized account. '
'Use the buttons at the bottom of this page to sign in.');
return null;
}

class HtmlLogger implements Logger {
Expand Down
15 changes: 15 additions & 0 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,21 @@ func (c *Cocoon) RefreshAgentAuthToken(agentID string) (*Agent, string, error) {
return agent, authToken, nil
}

// IsWhitelisted verifies that the given email address is whitelisted for access.
func (c *Cocoon) IsWhitelisted(email string) error {
query := datastore.NewQuery("WhitelistedAccount").
Filter("Email =", email).
Limit(20)
iter := query.Run(c.Ctx)
_, err := iter.Next(&WhitelistedAccount{})

if err == datastore.Done {
return fmt.Errorf("%v is not authorized to access the dashboard", email)
}

return nil
}

// allTaskStatuses contains all possible task statuses.
var allTaskStatuses = [...]TaskStatus{
TaskNew,
Expand Down
14 changes: 14 additions & 0 deletions db/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,17 @@ type Agent struct {
AuthTokenHash []byte
Capabilities []string
}

// WhitelistedAccount gives permission to access the dashboard to a specific
// Google account.
//
// In production an account can be added by an administrator using the
// Datastore web UI.
//
// The Datastore UI on the dev server is limited. To add an account make a
// HTTP GET call to:
//
// http://localhost:8080/api/whitelist-account?email=ACCOUNT_EMAIL
type WhitelistedAccount struct {
Email string
}