"x/ref/services/identity": JSON responses for HTTP Blessing service

This CL updates the HTTP service for exchanging an access token
for a blessing by taking an output_format in the request paramaters
and encoding the blessing response in the specified output format.
This CL adds support for the Base64-VOM and Base64-JSON output
formats.

Change-Id: I1ebea7e93a6bd8ec0850af9c8ec270c0b45230ff
diff --git a/runtime/internal/discovery/advertise.go b/runtime/internal/discovery/advertise.go
deleted file mode 100644
index 1422760..0000000
--- a/runtime/internal/discovery/advertise.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package discovery
-
-import (
-	"v.io/v23/context"
-	"v.io/v23/discovery"
-	"v.io/v23/security/access"
-)
-
-// Advertise implements discovery.Advertiser.
-func (ds *ds) Advertise(ctx *context.T, service discovery.Service, perms access.Permissions) error {
-	return nil
-}
diff --git a/runtime/internal/discovery/discovery.go b/runtime/internal/discovery/discovery.go
deleted file mode 100644
index 53501d7..0000000
--- a/runtime/internal/discovery/discovery.go
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package discovery
-
-// ds is an implementation of discovery.T.
-type ds struct {
-}
diff --git a/runtime/internal/discovery/plugin.go b/runtime/internal/discovery/plugin.go
deleted file mode 100644
index c275d89..0000000
--- a/runtime/internal/discovery/plugin.go
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package discovery
-
-// Plugin is the basic interface for a plugin to discovery service.
-type Plugin interface {
-}
diff --git a/runtime/internal/discovery/scan.go b/runtime/internal/discovery/scan.go
deleted file mode 100644
index f955326..0000000
--- a/runtime/internal/discovery/scan.go
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package discovery
-
-import (
-	"v.io/v23/context"
-	"v.io/v23/discovery"
-)
-
-// Scan implements discovery.Scanner.
-func (ds *ds) Scan(ctx *context.T, query string) (<-chan discovery.Update, error) {
-	return nil, nil
-}
diff --git a/services/device/mgmt_v23_test.go b/services/device/mgmt_v23_test.go
index 70593ad..23345a4 100644
--- a/services/device/mgmt_v23_test.go
+++ b/services/device/mgmt_v23_test.go
@@ -39,7 +39,6 @@
 	"errors"
 	"flag"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"math/rand"
 	"os"
@@ -47,7 +46,6 @@
 	"path/filepath"
 	"regexp"
 	"strings"
-	"sync"
 	"time"
 
 	"v.io/x/ref"
@@ -104,6 +102,7 @@
 
 func testCore(i *v23tests.T, appUser, deviceUser string, withSuid bool) {
 	defer fmt.Fprintf(os.Stderr, "--------------- SHUTDOWN ---------------\n")
+	userFlag := "--single_user"
 	tempDir := ""
 
 	if withSuid {
@@ -215,7 +214,7 @@
 	if withSuid {
 		deviceScriptArguments = append(deviceScriptArguments, "--devuser="+deviceUser)
 	} else {
-		deviceScriptArguments = append(deviceScriptArguments, "--single_user")
+		deviceScriptArguments = append(deviceScriptArguments, userFlag)
 	}
 
 	deviceScriptArguments = append(deviceScriptArguments, []string{
@@ -227,24 +226,9 @@
 
 	deviceScript.Start(deviceScriptArguments...).WaitOrDie(os.Stdout, os.Stderr)
 	deviceScript.Start("start").WaitOrDie(os.Stdout, os.Stderr)
-	dmLog := filepath.Join(dmInstallDir, "dmroot/device-manager/logs/deviced.INFO")
-	stopDevMgr := func() {
-		deviceScript.Run("stop")
-		if dmLogF, err := os.Open(dmLog); err != nil {
-			i.Errorf("Failed to read dm log: %v", err)
-		} else {
-			fmt.Fprintf(os.Stderr, "--------------- START DM LOG ---------------\n")
-			defer dmLogF.Close()
-			if _, err := io.Copy(os.Stderr, dmLogF); err != nil {
-				i.Errorf("Error dumping dm log: %v", err)
-			}
-			fmt.Fprintf(os.Stderr, "--------------- END DM LOG ---------------\n")
-		}
-	}
-	var stopDevMgrOnce sync.Once
-	defer stopDevMgrOnce.Do(stopDevMgr)
 	// Grab the endpoint for the claimable service from the device manager's
 	// log.
+	dmLog := filepath.Join(dmInstallDir, "dmroot/device-manager/logs/deviced.INFO")
 	var claimableEP string
 	expiry := time.Now().Add(30 * time.Second)
 	for {
@@ -512,7 +496,7 @@
 	mtEP = resolveChange(mtName, mtEP)
 
 	// Shut down the device manager.
-	stopDevMgrOnce.Do(stopDevMgr)
+	deviceScript.Run("stop")
 
 	// Wait for the mounttable entry to go away.
 	resolveGone := func(name string) string {
diff --git a/services/identity/internal/handlers/bless.go b/services/identity/internal/handlers/bless.go
index e7607c5..45142ac 100644
--- a/services/identity/internal/handlers/bless.go
+++ b/services/identity/internal/handlers/bless.go
@@ -6,6 +6,7 @@
 
 import (
 	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"strings"
@@ -20,9 +21,13 @@
 )
 
 const (
-	PublicKeyFormKey   = "public_key"
-	AccessTokenFormKey = "token"
-	CaveatsFormKey     = "caveats"
+	publicKeyFormKey    = "public_key"
+	tokenFormKey        = "token"
+	caveatsFormKey      = "caveats"
+	outputFormatFormKey = "output_format"
+
+	jsonFormat      = "json"
+	base64VomFormat = "base64vom"
 )
 
 type accessTokenBlesser struct {
@@ -30,7 +35,7 @@
 	params blesser.OAuthBlesserParams
 }
 
-// NewOAuthBlessingHandler returns an http.Handler that uses OAuth2 access tokens
+// NewOAuthBlessingHandler returns an http.Handler that uses Google OAuth2 Access tokens
 // to obtain the username of the requestor and reponds with blessings for that username.
 //
 // The blessings are namespaced under the ClientID for the access token. In particular,
@@ -41,11 +46,16 @@
 // RevocationManager is specified by the params or they carry an ExpiryCaveat that
 // expires after the duration specified by the params.
 //
-// The handler expects the following parameters in the form data sent during a
-// request
+// The handler expects the following request parameters:
 // - "public_key": Base64 DER encoded PKIX representation of the client's public key
-// - "caveats": Base64 VOM encoded list of caveats
-// - "token": OAuth2 access token
+// - "caveats": Base64 VOM encoded list of caveats [OPTIONAL]
+// - "token": Google OAuth2 Access token
+// - "output_format": The encoding format for the returned blessings. The following
+//   formats are supported:
+//     - "json": JSON-encoding of the wire format of Blessings.
+//     - "base64vom": Base64 encoding of VOM-encoded Blessings [DEFAULT]
+//
+// The response consists of blessings encoded in the requested output format.
 //
 // WARNINGS:
 //   - There is no binding between the channel over which the access token
@@ -59,16 +69,17 @@
 }
 
 func (a *accessTokenBlesser) blessingCaveats(r *http.Request, p security.Principal) ([]security.Caveat, error) {
-	caveatsVom, err := base64.URLEncoding.DecodeString(r.FormValue(CaveatsFormKey))
-	if err != nil {
-		return nil, fmt.Errorf("failed to base64 decode caveats: %v", err)
-	}
-
 	var caveats []security.Caveat
-	if err := vom.Decode(caveatsVom, &caveats); err != nil {
-		return nil, fmt.Errorf("failed to VOM decode caveats: %v", err)
-	}
+	if base64VomCaveats := r.FormValue(caveatsFormKey); len(base64VomCaveats) != 0 {
+		vomCaveats, err := base64.URLEncoding.DecodeString(base64VomCaveats)
+		if err != nil {
+			return nil, fmt.Errorf("base64.URLEncoding.DecodeString failed: %v", err)
+		}
 
+		if err := vom.Decode(vomCaveats, &caveats); err != nil {
+			return nil, fmt.Errorf("vom.Decode failed: %v", err)
+		}
+	}
 	// TODO(suharshs, ataly): Should we ensure that we have at least a
 	// revocation or expiry caveat?
 	if len(caveats) == 0 {
@@ -91,15 +102,15 @@
 }
 
 func (a *accessTokenBlesser) remotePublicKey(r *http.Request) (security.PublicKey, error) {
-	publicKeyVom, err := base64.URLEncoding.DecodeString(r.FormValue(PublicKeyFormKey))
+	publicKeyVom, err := base64.URLEncoding.DecodeString(r.FormValue(publicKeyFormKey))
 	if err != nil {
-		return nil, fmt.Errorf("failed to base64 decode public key: %v", err)
+		return nil, fmt.Errorf("base64.URLEncoding.DecodeString failed: %v", err)
 	}
 	return security.UnmarshalPublicKey(publicKeyVom)
 }
 
 func (a *accessTokenBlesser) blessingExtension(r *http.Request) (string, error) {
-	email, clientID, err := a.params.OAuthProvider.GetEmailAndClientID(r.FormValue(AccessTokenFormKey))
+	email, clientID, err := a.params.OAuthProvider.GetEmailAndClientID(r.FormValue(tokenFormKey))
 	if err != nil {
 		return "", err
 	}
@@ -116,11 +127,23 @@
 	return strings.Join([]string{clientID, email}, security.ChainSeparator), nil
 }
 
+func (a *accessTokenBlesser) encodeBlessingsJson(b security.Blessings) ([]byte, error) {
+	return json.Marshal(security.MarshalBlessings(b))
+}
+
+func (a *accessTokenBlesser) encodeBlessingsVom(b security.Blessings) (string, error) {
+	bVom, err := vom.Encode(b)
+	if err != nil {
+		return "", fmt.Errorf("vom.Encode(%v) failed: %v", b, err)
+	}
+	return base64.URLEncoding.EncodeToString(bVom), nil
+}
+
 func (a *accessTokenBlesser) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	remoteKey, err := a.remotePublicKey(r)
 	if err != nil {
 		a.ctx.Info("Failed to decode public key [%v] for request %#v", err, r)
-		util.HTTPServerError(w, err)
+		util.HTTPServerError(w, fmt.Errorf("failed to decode public key: %v", err))
 		return
 	}
 
@@ -130,31 +153,50 @@
 	caveats, err := a.blessingCaveats(r, p)
 	if err != nil {
 		a.ctx.Info("Failed to constuct caveats for blessing [%v] for request %#v", err, r)
-		util.HTTPServerError(w, err)
+		util.HTTPServerError(w, fmt.Errorf("failed to construct caveats for blessing: %v", err))
 		return
 	}
 
 	extension, err := a.blessingExtension(r)
 	if err != nil {
 		a.ctx.Info("Failed to process access token [%v] for request %#v", err, r)
-		util.HTTPServerError(w, err)
+		util.HTTPServerError(w, fmt.Errorf("failed to process access token: %v", err))
 		return
 	}
 
 	blessings, err := p.Bless(remoteKey, with, extension, caveats[0], caveats[1:]...)
 	if err != nil {
 		a.ctx.Info("Failed to Bless [%v] for request %#v", err, r)
-		util.HTTPServerError(w, fmt.Errorf("Bless failed: %v", err))
+		util.HTTPServerError(w, fmt.Errorf("failed to Bless: %v", err))
 		return
 	}
 
-	blessingsVom, err := vom.Encode(blessings)
-	if err != nil {
-		a.ctx.Info("Failed to VOM encode blessings [%v] for request %#v", err, r)
-		util.HTTPServerError(w, fmt.Errorf("failed to VOM encode blessings: %v", err))
+	outputFormat := r.FormValue(outputFormatFormKey)
+	if len(outputFormat) == 0 {
+		outputFormat = base64VomFormat
+	}
+	switch outputFormat {
+	case jsonFormat:
+		encodedBlessings, err := a.encodeBlessingsJson(blessings)
+		if err != nil {
+			a.ctx.Info("Failed to encode blessings [%v] for request %#v", err, r)
+			util.HTTPServerError(w, fmt.Errorf("failed to encode blessings in format %v: %v", outputFormat, err))
+			return
+		}
+		w.Header().Set("Content-Type", "application/json")
+		w.Write(encodedBlessings)
+	case base64VomFormat:
+		encodedBlessings, err := a.encodeBlessingsVom(blessings)
+		if err != nil {
+			a.ctx.Info("Failed to encode blessings [%v] for request %#v", err, r)
+			util.HTTPServerError(w, fmt.Errorf("failed to encode blessings in format %v: %v", outputFormat, err))
+			return
+		}
+		w.Header().Set("Content-Type", "application/text")
+		w.Write([]byte(encodedBlessings))
+	default:
+		a.ctx.Info("Unrecognized output format [%v] in request %#v", outputFormat, r)
+		util.HTTPServerError(w, fmt.Errorf("unrecognized output format [%v] in request. Allowed formats are [%v, %v]", outputFormat, base64VomFormat, jsonFormat))
 		return
 	}
-	blessingsVomB64 := base64.URLEncoding.EncodeToString(blessingsVom)
-	w.Header().Set("Content-Type", "application/text")
-	w.Write([]byte(blessingsVomB64))
 }
diff --git a/services/identity/internal/handlers/handlers_test.go b/services/identity/internal/handlers/handlers_test.go
index 3740451..5046b7f 100644
--- a/services/identity/internal/handlers/handlers_test.go
+++ b/services/identity/internal/handlers/handlers_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package handlers_test
+package handlers
 
 import (
 	"bytes"
@@ -24,7 +24,6 @@
 	_ "v.io/x/ref/runtime/factories/generic"
 	"v.io/x/ref/services/identity"
 	"v.io/x/ref/services/identity/internal/blesser"
-	"v.io/x/ref/services/identity/internal/handlers"
 	"v.io/x/ref/services/identity/internal/oauth"
 	"v.io/x/ref/services/identity/internal/revocation"
 	"v.io/x/ref/test"
@@ -36,7 +35,7 @@
 	blessingNames := []string{"test-root"}
 	p := testutil.NewPrincipal(blessingNames...)
 
-	ts := httptest.NewServer(handlers.BlessingRoot{p})
+	ts := httptest.NewServer(BlessingRoot{p})
 	defer ts.Close()
 	response, err := http.Get(ts.URL)
 	if err != nil {
@@ -69,7 +68,7 @@
 	}
 }
 
-func TestBlessUsingAccessToken(t *testing.T) {
+func TestBless(t *testing.T) {
 	var (
 		blesserPrin = testutil.NewPrincipal("blesser")
 		blesseePrin = testutil.NewPrincipal("blessee")
@@ -77,35 +76,59 @@
 		methodCav, _ = security.NewMethodCaveat("foo")
 		expiryCav, _ = security.NewExpiryCaveat(time.Now().Add(time.Hour))
 
-		mkReqURL = func(baseURLStr string, caveats []security.Caveat) string {
+		mkReqURL = func(baseURLStr string, caveats []security.Caveat, outputFormat string) string {
 			baseURL, err := url.Parse(baseURLStr)
 			if err != nil {
 				t.Fatal(err)
 			}
-			caveatsVom, err := vom.Encode(caveats)
-			if err != nil {
-				t.Fatal(err)
+			params := url.Values{}
+
+			if len(caveats) != 0 {
+				caveatsVom, err := vom.Encode(caveats)
+				if err != nil {
+					t.Fatal(err)
+				}
+				params.Add(caveatsFormKey, base64.URLEncoding.EncodeToString(caveatsVom))
 			}
 			keyBytes, err := blesseePrin.PublicKey().MarshalBinary()
 			if err != nil {
 				t.Fatal(err)
 			}
-			params := url.Values{}
-			params.Add(handlers.CaveatsFormKey, base64.URLEncoding.EncodeToString(caveatsVom))
-			params.Add(handlers.PublicKeyFormKey, base64.URLEncoding.EncodeToString(keyBytes))
-			params.Add(handlers.AccessTokenFormKey, "mocktoken")
+			params.Add(publicKeyFormKey, base64.URLEncoding.EncodeToString(keyBytes))
+			params.Add(tokenFormKey, "mocktoken")
+			params.Add(outputFormatFormKey, outputFormat)
+
 			baseURL.RawQuery = params.Encode()
 			return baseURL.String()
 		}
 
-		decodeBlessings = func(blessingsVomB64 string) security.Blessings {
-			blessingsVom, err := base64.URLEncoding.DecodeString(blessingsVomB64)
+		vomRoundTrip = func(in, out interface{}) error {
+			data, err := vom.Encode(in)
 			if err != nil {
-				t.Fatal(err)
+				return err
+			}
+			return vom.Decode(data, out)
+		}
+
+		decodeBlessings = func(b []byte, outputFormat string) security.Blessings {
+			if len(outputFormat) == 0 {
+				outputFormat = base64VomFormat
 			}
 			var res security.Blessings
-			if err := vom.Decode(blessingsVom, &res); err != nil {
-				t.Fatal(err)
+			switch outputFormat {
+			case base64VomFormat:
+				if raw, err := base64.URLEncoding.DecodeString(string(b)); err != nil {
+					t.Fatal(err)
+				} else if err = vom.Decode(raw, &res); err != nil {
+					t.Fatal(err)
+				}
+			case jsonFormat:
+				var wb security.WireBlessings
+				if err := json.Unmarshal(b, &wb); err != nil {
+					t.Fatal(err)
+				} else if err = vomRoundTrip(wb, &res); err != nil {
+					t.Fatal(err)
+				}
 			}
 			return res
 		}
@@ -155,49 +178,50 @@
 		},
 	}
 	for _, testcase := range testcases {
-		ts := httptest.NewServer(handlers.NewOAuthBlessingHandler(ctx, testcase.params))
-		defer ts.Close()
+		for _, outputFormat := range []string{jsonFormat, base64VomFormat, ""} {
+			ts := httptest.NewServer(NewOAuthBlessingHandler(ctx, testcase.params))
+			defer ts.Close()
 
-		response, err := http.Get(mkReqURL(ts.URL, testcase.caveats))
-		if err != nil {
-			t.Fatal(err)
-		}
-		blessingsVomB64, err := ioutil.ReadAll(response.Body)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		blessings := decodeBlessings(string(blessingsVomB64))
-
-		// Blessing should be bound to the blessee.
-		if got, want := blessings.PublicKey(), blesseePrin.PublicKey(); !reflect.DeepEqual(got, want) {
-			t.Errorf("got blessings for public key %v, want blessings for public key %v", got, want)
-		}
-
-		// Verify the name and caveats on the blessings.
-		binfo := blesseePrin.BlessingsInfo(blessings)
-		if len(binfo) != 1 {
-			t.Errorf("got blessings with %d names, want blessings with 1 name", len(binfo))
-		}
-		wantName := "blesser" + security.ChainSeparator + testClientID + security.ChainSeparator + testEmail
-		caveats, ok := binfo[wantName]
-		if !ok {
-			t.Errorf("expected blessing with name %v, got none", wantName)
-		}
-
-		if len(testcase.caveats) > 0 {
-			// The blessing must have exactly those caveats that were provided in the request.
-			if !caveatsMatch(t, caveats, testcase.caveats) {
-				t.Errorf("got blessings with caveats %v, want blessings with caveats %v", caveats, testcase.caveats)
+			response, err := http.Get(mkReqURL(ts.URL, testcase.caveats, outputFormat))
+			if err != nil {
+				t.Fatal(err)
 			}
-		} else if len(caveats) != 1 {
-			t.Errorf("got blessings with %d caveats, want blessings with 1 caveats", len(caveats))
-		} else if testcase.params.RevocationManager != nil && caveats[0].Id != security.PublicKeyThirdPartyCaveat.Id {
-			// The blessing must have a third-party revocation caveat.
-			t.Errorf("got blessings with caveat (%v), want blessings with a PublicKeyThirdPartyCaveat", caveats[0].Id)
-		} else if testcase.params.RevocationManager == nil && caveats[0].Id != security.ExpiryCaveat.Id {
-			// The blessing must have an expiry caveat.
-			t.Errorf("got blessings with caveat (%v), want blessings with an ExpiryCaveat", caveats[0].Id)
+			b, err := ioutil.ReadAll(response.Body)
+			if err != nil {
+				t.Fatal(err)
+			}
+			blessings := decodeBlessings(b, outputFormat)
+
+			// Blessing should be bound to the blessee.
+			if got, want := blessings.PublicKey(), blesseePrin.PublicKey(); !reflect.DeepEqual(got, want) {
+				t.Errorf("got blessings for public key %v, want blessings for public key %v", got, want)
+			}
+
+			// Verify the name and caveats on the blessings.
+			binfo := blesseePrin.BlessingsInfo(blessings)
+			if len(binfo) != 1 {
+				t.Errorf("got blessings with %d names, want blessings with 1 name", len(binfo))
+			}
+			wantName := "blesser" + security.ChainSeparator + testClientID + security.ChainSeparator + testEmail
+			caveats, ok := binfo[wantName]
+			if !ok {
+				t.Errorf("expected blessing with name %v, got none", wantName)
+			}
+
+			if len(testcase.caveats) > 0 {
+				// The blessing must have exactly those caveats that were provided in the request.
+				if !caveatsMatch(t, caveats, testcase.caveats) {
+					t.Errorf("got blessings with caveats %v, want blessings with caveats %v", caveats, testcase.caveats)
+				}
+			} else if len(caveats) != 1 {
+				t.Errorf("got blessings with %d caveats, want blessings with 1 caveats", len(caveats))
+			} else if testcase.params.RevocationManager != nil && caveats[0].Id != security.PublicKeyThirdPartyCaveat.Id {
+				// The blessing must have a third-party revocation caveat.
+				t.Errorf("got blessings with caveat (%v), want blessings with a PublicKeyThirdPartyCaveat", caveats[0].Id)
+			} else if testcase.params.RevocationManager == nil && caveats[0].Id != security.ExpiryCaveat.Id {
+				// The blessing must have an expiry caveat.
+				t.Errorf("got blessings with caveat (%v), want blessings with an ExpiryCaveat", caveats[0].Id)
+			}
 		}
 	}
 }