"veyron/tools/principal": Part 4: seekblessings

Add support for the "seekblessings" command to the principal
tool. The command expects the Veyron Identity Server to provide
a security.WireBlessings (as opposed to a serialzied PublicID)
in exchange for a Macaroon obtained from the OAuth flow described
in:

https://docs.google.com/a/google.com/document/d/1SRoc2cKE9iE1fWR7aSmMoccZoi4ZE8BQL7sr1LDNVkk/edit

Change-Id: I200caa7807531ea5cc21ac534879a7c39927f6a1
diff --git a/runtimes/google/ipc/stream/vc/init.go b/runtimes/google/ipc/stream/vc/init.go
index 8b7dbaa..96c21bd 100644
--- a/runtimes/google/ipc/stream/vc/init.go
+++ b/runtimes/google/ipc/stream/vc/init.go
@@ -59,3 +59,7 @@
 func (s *anonymousBlessingStore) PublicKey() security.PublicKey {
 	return s.k
 }
+
+func (anonymousBlessingStore) DebugString() string {
+	return "anonymous BlessingStore"
+}
diff --git a/tools/principal/bless.go b/tools/principal/bless.go
new file mode 100644
index 0000000..578cc03
--- /dev/null
+++ b/tools/principal/bless.go
@@ -0,0 +1,135 @@
+package main
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+	"fmt"
+	"html/template"
+	"net"
+	"net/http"
+	"net/url"
+	"os"
+	"os/exec"
+	"strings"
+
+	"veyron.io/veyron/veyron/services/identity/googleoauth"
+	"veyron.io/veyron/veyron2/vlog"
+)
+
+func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string) (<-chan string, error) {
+	// Setup a HTTP server to recieve a blessing macaroon from the identity server.
+	// Steps:
+	// 1. Generate a state token to be included in the HTTP request
+	//    (though, arguably, the random port assigment for the HTTP server is enough
+	//    for XSRF protection)
+	// 2. Setup a HTTP server which will receive the final blessing macaroon from the id server.
+	// 3. Print out the link (to start the auth flow) for the user to click.
+	// 4. Return the macaroon and the rpc object name(where to make the MacaroonBlesser.Bless RPC call)
+	//    in the "result" channel.
+	var stateBuf [32]byte
+	if _, err := rand.Read(stateBuf[:]); err != nil {
+		return nil, fmt.Errorf("failed to generate state token for OAuth: %v", err)
+	}
+	state := base64.URLEncoding.EncodeToString(stateBuf[:])
+
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		return nil, fmt.Errorf("failed to setup authorization code interception server: %v", err)
+	}
+	result := make(chan string)
+
+	redirectURL := fmt.Sprintf("http://%s/macaroon", ln.Addr())
+	http.HandleFunc("/macaroon", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/html")
+		tmplArgs := struct {
+			Blessings, ErrShort, ErrLong string
+		}{}
+		defer func() {
+			if len(tmplArgs.ErrShort) > 0 {
+				w.WriteHeader(http.StatusBadRequest)
+			}
+			if err := tmpl.Execute(w, tmplArgs); err != nil {
+				vlog.Info("Failed to render template:", err)
+			}
+		}()
+
+		toolState := r.FormValue("state")
+		if toolState != state {
+			tmplArgs.ErrShort = "Unexpected request"
+			tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forgery?"
+			return
+		}
+		result <- r.FormValue("macaroon")
+		result <- r.FormValue("object_name")
+		defer close(result)
+		blessed, ok := <-blessedChan
+		if !ok {
+			tmplArgs.ErrShort = "No blessings received"
+			tmplArgs.ErrLong = "Unable to obtain blessings from the Veyron service"
+			return
+		}
+		tmplArgs.Blessings = blessed
+		ln.Close()
+	})
+	go http.Serve(ln, nil)
+
+	// Print the link to start the flow.
+	url, err := seekBlessingsURL(blessServerURL, redirectURL, state)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create seekBlessingsURL: %s", err)
+	}
+	fmt.Fprintln(os.Stderr, "Please visit the following URL to seek blessings:")
+	fmt.Fprintln(os.Stderr, url)
+	// Make an attempt to start the browser as a convenience.
+	// If it fails, doesn't matter - the client can see the URL printed above.
+	// Use exec.Command().Start instead of exec.Command().Run since there is no
+	// need to wait for the command to return (and indeed on some window managers,
+	// the command will not exit until the browser is closed).
+	if len(openCommand) != 0 {
+		exec.Command(openCommand, url).Start()
+	}
+	return result, nil
+}
+
+func seekBlessingsURL(blessServerURL, redirectURL, state string) (string, error) {
+	baseURL, err := url.Parse(joinURL(blessServerURL, googleoauth.SeekBlessingsRoute))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse url: %v", err)
+	}
+	params := url.Values{}
+	params.Add("redirect_url", redirectURL)
+	params.Add("state", state)
+	baseURL.RawQuery = params.Encode()
+	return baseURL.String(), nil
+}
+
+func joinURL(baseURL, suffix string) string {
+	if !strings.HasSuffix(baseURL, "/") {
+		baseURL += "/"
+	}
+	return baseURL + suffix
+}
+
+var tmpl = template.Must(template.New("name").Parse(`<!doctype html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Veyron Identity: Google</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+{{if .Blessings}}
+<!--Attempt to close the window. Though this script does not work on many browser configurations-->
+<script type="text/javascript">window.close();</script>
+{{end}}
+</head>
+<body>
+<div class="container">
+{{if .ErrShort}}
+<h1><span class="label label-danger">error</span>{{.ErrShort}}</h1>
+<div class="well">{{.ErrLong}}</div>
+{{else}}
+<h3>Received blessings: <tt>{{.Blessings}}</tt></h3>
+{{end}}
+</div>
+</body>
+</html>`))
diff --git a/tools/principal/main.go b/tools/principal/main.go
index 305d985..a0a61c1 100644
--- a/tools/principal/main.go
+++ b/tools/principal/main.go
@@ -10,10 +10,12 @@
 	"time"
 
 	"veyron.io/veyron/veyron/lib/cmdline"
+	"veyron.io/veyron/veyron/services/identity"
 	"veyron.io/veyron/veyron/services/identity/util"
 
 	"veyron.io/veyron/veyron2/rt"
 	"veyron.io/veyron/veyron2/security"
+	"veyron.io/veyron/veyron2/vdl/vdlutil"
 )
 
 var (
@@ -21,6 +23,11 @@
 	flagBlessFor   time.Duration
 	flagAddForPeer string
 
+	// Flags for the "seekblessing" command
+	flagSeekBlessingFrom string
+	flagSkipSetDefault   bool
+	flagForPeer          string
+
 	cmdDump = &cmdline.Command{
 		Name:  "dump",
 		Short: "Dump out information about the principal",
@@ -215,6 +222,67 @@
 			return nil
 		},
 	}
+
+	cmdSeekBlessings = &cmdline.Command{
+		Name:  "seekblessings",
+		Short: "Seek blessings from a web-based Veyron blesser",
+		Long: `
+Seeks blessings from a web-based Veyron blesser which
+requires the caller to first authenticate with Google using OAuth. Simply
+run the command to see what happens.
+
+The blessings are sought for the principal specified by the environment
+(VEYRON_CREDENTIALS) that this tool is running in.
+
+The blessings obtained are set as default, unless a --skip_set_default flag
+is provided, and are also set for sharing with all peers, unless a more
+specific peer pattern is provided using the --for_peer flag.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			blessedChan := make(chan string)
+			defer close(blessedChan)
+			macaroonChan, err := getMacaroonForBlessRPC(flagSeekBlessingFrom, blessedChan)
+			if err != nil {
+				return fmt.Errorf("failed to get macaroon from Veyron blesser: %v", err)
+			}
+			macaroon := <-macaroonChan
+			service := <-macaroonChan
+
+			ctx, cancel := rt.R().NewContext().WithTimeout(time.Minute)
+			defer cancel()
+
+			var reply vdlutil.Any
+			blesser, err := identity.BindMacaroonBlesser(service)
+			if err == nil {
+				reply, err = blesser.Bless(ctx, macaroon)
+			}
+			if err != nil {
+				return fmt.Errorf("failed to get blessing from %q: %v", service, err)
+			}
+			wire, ok := reply.(security.WireBlessings)
+			if !ok {
+				return fmt.Errorf("received %T, want security.WireBlessings", reply)
+			}
+			blessings, err := security.NewBlessings(wire)
+			if err != nil {
+				return fmt.Errorf("failed to construct Blessings object from wire data: %v", err)
+			}
+			blessedChan <- fmt.Sprint(blessings)
+			// Wait for getTokenForBlessRPC to clean up:
+			<-macaroonChan
+
+			if !flagSkipSetDefault {
+				if err := rt.R().Principal().BlessingStore().SetDefault(blessings); err != nil {
+					return fmt.Errorf("failed to set blessings %v as default: %v", blessings, err)
+				}
+			}
+			pattern := security.BlessingPattern(flagForPeer)
+			if _, err := rt.R().Principal().BlessingStore().Set(blessings, pattern); err != nil {
+				return fmt.Errorf("failed to set blessings %v for peers %v: %v", blessings, pattern, err)
+			}
+			return dumpBlessings(blessings)
+		},
+	}
 )
 
 func main() {
@@ -225,6 +293,9 @@
 	}
 	rt.Init()
 	cmdBlessSelf.Flags.DurationVar(&flagBlessFor, "for", 0*time.Hour, "Expiry time of Blessing (optional)")
+	cmdSeekBlessings.Flags.StringVar(&flagSeekBlessingFrom, "from", "https://proxy.envyor.com:8125/google", "URL to use to begin the seek blessings process")
+	cmdSeekBlessings.Flags.BoolVar(&flagSkipSetDefault, "skip_set_default", false, "flag to indicate that the blessings obtained from the Veyron blesser must not be set as default on the principals's blessing store")
+	cmdSeekBlessings.Flags.StringVar(&flagForPeer, "for_peer", "...", "pattern to be used while setting the blessings obtained from the Veyron blesser on the principal's blessing store")
 
 	(&cmdline.Command{
 		Name:  "principal",
@@ -235,7 +306,7 @@
 
 All objects are printed using base64-VOM-encoding.
 `,
-		Children: []*cmdline.Command{cmdDump, cmdPrint, cmdBlessSelf, cmdDefault, cmdForPeer, cmdSetDefault, cmdSet},
+		Children: []*cmdline.Command{cmdDump, cmdPrint, cmdBlessSelf, cmdDefault, cmdForPeer, cmdSetDefault, cmdSet, cmdSeekBlessings},
 	}).Main()
 }
 
diff --git a/tools/principal/main_darwin.go b/tools/principal/main_darwin.go
new file mode 100644
index 0000000..bceafd2
--- /dev/null
+++ b/tools/principal/main_darwin.go
@@ -0,0 +1,5 @@
+// +build darwin
+
+package main
+
+const openCommand = "open"
diff --git a/tools/principal/main_linux.go b/tools/principal/main_linux.go
new file mode 100644
index 0000000..cb73c65
--- /dev/null
+++ b/tools/principal/main_linux.go
@@ -0,0 +1,5 @@
+// +build linux
+
+package main
+
+const openCommand = "xdg-open"
diff --git a/tools/principal/main_nacl.go b/tools/principal/main_nacl.go
new file mode 100644
index 0000000..9dea3b5
--- /dev/null
+++ b/tools/principal/main_nacl.go
@@ -0,0 +1,3 @@
+package main
+
+const openCommand = ""
diff --git a/tools/principal/test.sh b/tools/principal/test.sh
index 4335c66..770abb3 100755
--- a/tools/principal/test.sh
+++ b/tools/principal/test.sh
@@ -3,6 +3,8 @@
 # Test the principal command-line tool.
 #
 # This tests most operations of the principal command-line tool.
+# Not the "seekblessing" command yet, since that requires
+# starting a separate server.
 
 source "${VEYRON_ROOT}/scripts/lib/shell_test.sh"