veyron/services/identity/identityd: Auditing and display.

This commit provides an implementation of the audit.Auditor interface
that writes out all audit events synchronously to a file.

Furthermore:
(1) the identityd daemon uses this Auditor implementation to
log all activity on its private key.
(2) the oauth  HTTP handler is repurposed to display all blessings
provided by identityd for the user

identityd thus now never sends private keys over the network
when configured to use google as a provider of names. Private
keys are still sent by the "random" HTTP handler, which I intend
to remove once javascript/WSPR related tests do not rely on it.

Change-Id: If99ca1ff4735e9723c533c4107cf8a82f75e93e0
diff --git a/services/identity/auditor/file.go b/services/identity/auditor/file.go
new file mode 100644
index 0000000..f5639e4
--- /dev/null
+++ b/services/identity/auditor/file.go
@@ -0,0 +1,116 @@
+package auditor
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"sync"
+	"time"
+
+	"veyron/security/audit"
+	"veyron2/security/wire"
+	"veyron2/vlog"
+	"veyron2/vom"
+)
+
+type fileAuditor struct {
+	mu   sync.Mutex
+	file *os.File
+	enc  *vom.Encoder
+}
+
+// NewFileAuditor returns a security.Auditor implementation that synchronously writes
+// audit log entries to files on disk.
+//
+// The file on disk is named with the provided prefix and a timestamp appended to it.
+//
+// TODO(ashankar,ataly): Should we be using something more query-able, like sqllite or
+// something? Do we need integrity protection (NewSigningWriter)?
+func NewFileAuditor(prefix string) (audit.Auditor, error) {
+	var file *os.File
+	for file == nil {
+		fname := fmt.Sprintf("%s.%s", prefix, time.Now().Format(time.RFC3339))
+		var err error
+		if file, err = os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600); err != nil && !os.IsExist(err) {
+			return nil, err
+		}
+	}
+	return &fileAuditor{
+		file: file,
+		enc:  vom.NewEncoder(file),
+	}, nil
+}
+
+func (a *fileAuditor) Audit(entry audit.Entry) error {
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	if err := a.enc.Encode(entry); err != nil {
+		return fmt.Errorf("failed to write audit entry: %v", err)
+	}
+	// Ensure that any log entries are not lost in case the machine on which this process is running reboots.
+	if err := a.file.Sync(); err != nil {
+		return fmt.Errorf("failed to commit audit entry to persistent storage: %v", err)
+	}
+	return nil
+}
+
+// ReadAuditLog reads audit entries written using NewFileAuditor and produces them on the returned channel.
+//
+// The returned channel is closed when all committed entries have been sent on the channel.
+// If blessingFilter is non-empty, only audit entries for method invocations matching:
+// security.PrivateID.Bless(<*>, blessingFilter, ...)
+// will be printed on the channel.
+//
+// TODO(ashankar): This custom "query" language is another reason why I think of a more sophisticated store.
+func ReadAuditLog(prefix, blessingFilter string) (<-chan audit.Entry, error) {
+	files, err := filepath.Glob(prefix + "*")
+	if err != nil {
+		return nil, err
+	}
+	c := make(chan audit.Entry)
+	go sendAuditEvents(c, files, blessingFilter)
+	return c, nil
+}
+
+func sendAuditEvents(dst chan<- audit.Entry, src []string, blessingFilter string) {
+	defer close(dst)
+	for _, fname := range src {
+		file, err := os.Open(fname)
+		if err != nil {
+			vlog.VI(3).Infof("Unable to open %q: %v", fname, err)
+			continue
+		}
+		decoder := vom.NewDecoder(file)
+	fileloop:
+		for {
+			var entry audit.Entry
+			switch err := decoder.Decode(&entry); err {
+			case nil:
+				filterAndSend(&entry, blessingFilter, dst)
+			case io.EOF:
+				break fileloop
+			default:
+				vlog.Errorf("Corrupt audit log? %q: %v", fname, err)
+				break fileloop
+			}
+		}
+		file.Close()
+	}
+}
+
+func filterAndSend(entry *audit.Entry, filter string, dst chan<- audit.Entry) {
+	if err := wire.ValidateBlessingName(filter); err != nil {
+		// If the filter is invalid, send all events.
+		// This allows for a means to ask for all events without defining a new
+		// special character.
+		dst <- *entry
+		return
+	}
+	if entry.Method != "Bless" || len(entry.Arguments) < 2 {
+		return
+	}
+	if name, ok := entry.Arguments[1].(string); ok && name == filter {
+		dst <- *entry
+	}
+}
diff --git a/services/identity/auditor/file_test.go b/services/identity/auditor/file_test.go
new file mode 100644
index 0000000..3b70537
--- /dev/null
+++ b/services/identity/auditor/file_test.go
@@ -0,0 +1,91 @@
+package auditor
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+	"reflect"
+	"testing"
+	"time"
+
+	"veyron/security/audit"
+	"veyron2/rt"
+	"veyron2/security"
+)
+
+func TestFileAuditor(t *testing.T) {
+	r := rt.Init()
+	defer r.Cleanup()
+
+	// Setup a directory where the audit logs will be stored.
+	dir, err := ioutil.TempDir("", "TestFileAuditor")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	prefix := path.Join(dir, "log")
+	auditor, err := NewFileAuditor(prefix)
+	if err != nil {
+		t.Fatal(err)
+	}
+	provider, _ := r.NewIdentity("provider")
+	provider = audit.NewPrivateID(provider, auditor)
+	blessee := func() security.PublicID {
+		priv, _ := r.NewIdentity("blessee")
+		return priv.PublicID()
+	}()
+
+	// Bless a bunch of names
+	for idx, name := range []string{"brucewayne", "batman", "bugsbunny", "daffyduck", "batman", "bugsbunny", "brucewayne"} {
+		if _, err := provider.Bless(blessee, name, time.Hour, nil); err != nil {
+			t.Fatalf("Bless(#%d, %v, ...) failed: %v", idx, name, err)
+		}
+	}
+
+	tests := []struct {
+		Filter string
+		Want   []string
+	}{
+		{"", []string{"brucewayne", "batman", "bugsbunny", "daffyduck", "batman", "bugsbunny", "brucewayne"}},
+		{"/", []string{"brucewayne", "batman", "bugsbunny", "daffyduck", "batman", "bugsbunny", "brucewayne"}},
+		{"batman", []string{"batman", "batman"}},
+		{"daffyduck", []string{"daffyduck"}},
+		{"elmerfudd", []string{}},
+	}
+	for _, test := range tests {
+		ch, err := ReadAuditLog(prefix, test.Filter)
+		if err != nil {
+			t.Errorf("ReadAuditLog(%q, %q) failed: %v", prefix, test.Filter, err)
+			continue
+		}
+	channelloop:
+		for i, want := range test.Want {
+			got := <-ch
+			testinfo := fmt.Sprintf("in test %+v (%d/%d)", test, i, len(want))
+			if got.Method != "Bless" {
+				t.Errorf("Got %q, wanted the Bless method %s", got, testinfo)
+				break channelloop
+			}
+			if arg := got.Arguments[0]; !reflect.DeepEqual(arg, blessee) {
+				t.Errorf("First argument was %v, wanted %v", arg, blessee, testinfo)
+				break channelloop
+			}
+			if arg := got.Arguments[1].(string); arg != want {
+				t.Errorf("Second argument was %v, wanted %v %s", arg, want, testinfo)
+				break channelloop
+			}
+		}
+		select {
+		case entry, open := <-ch:
+			if open {
+				for _ = range ch {
+					// Drain the channel to prevent the producer goroutines from being leaked.
+				}
+				t.Errorf("Got more entries that expected (such as %v) for test %+v", entry, test)
+			}
+		default:
+		}
+	}
+}
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 6ddc455..ac9104b 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -1,6 +1,6 @@
 // Package googleoauth implements an http.Handler that uses OAuth 2.0 to
-// authenticate with Google and then generates a Veyron identity and
-// blesses it with the identity of the HTTP server.
+// authenticate with Google and then renders a page that displays all the
+// blessings that were provided for that Google user.
 //
 // The GoogleIDToken is currently validated by sending an HTTP request to
 // googleapis.com.  This adds a round-trip and service may be denied by
@@ -16,12 +16,11 @@
 	"net/http"
 	"path"
 	"strings"
-	"time"
 
 	"code.google.com/p/goauth2/oauth"
 
+	"veyron/services/identity/auditor"
 	"veyron/services/identity/util"
-	"veyron2"
 	"veyron2/vlog"
 )
 
@@ -34,17 +33,19 @@
 	// client_id and client_secret registered with the Google Developer
 	// Console for API access.
 	ClientID, ClientSecret string
-	// Minimum expiry time (in days) of identities issued by the server
-	MinExpiryDays int
-	// Runtime from which the identity of the server itself will be extracted,
-	// and new identities will be created for blessing.
-	Runtime veyron2.Runtime
-	// When set, restricts the allowed email addresses to this domain, e.g.
-	// google.com.
-	RestrictEmailDomain string
+	// Prefix for the audit log from which data will be sourced.
+	// (auditor.ReadAuditLog).
+	Auditor string
 }
 
-func (a *HandlerArgs) redirectURL() string { return path.Join(a.Addr, "oauth2callback") }
+func (a *HandlerArgs) redirectURL() string {
+	ret := a.Addr
+	if !strings.HasSuffix(ret, "/") {
+		ret += "/"
+	}
+	ret += "oauth2callback"
+	return ret
+}
 
 // URL used to verify google tokens.
 // (From https://developers.google.com/accounts/docs/OAuth2Login#validatinganidtoken
@@ -56,7 +57,7 @@
 // identity and bless it with the Google email address.
 func NewHandler(args HandlerArgs) http.Handler {
 	config := NewOAuthConfig(args.ClientID, args.ClientSecret, args.redirectURL())
-	return &handler{config, args.MinExpiryDays, util.NewCSRFCop(), args.Runtime, args.RestrictEmailDomain}
+	return &handler{config, util.NewCSRFCop(), args.Auditor}
 }
 
 // NewOAuthConfig returns the oauth.Config required for obtaining just the email address from Google using OAuth 2.0.
@@ -72,11 +73,9 @@
 }
 
 type handler struct {
-	config        *oauth.Config
-	minExpiryDays int
-	csrfCop       *util.CSRFCop
-	rt            veyron2.Runtime
-	domain        string
+	config  *oauth.Config
+	csrfCop *util.CSRFCop
+	auditor string
 }
 
 func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -111,27 +110,18 @@
 		util.HTTPBadRequest(w, r, err)
 		return
 	}
-	if len(h.domain) > 0 && !strings.HasSuffix(email, "@"+h.domain) {
-		util.HTTPServerError(w, fmt.Errorf("email domain in %s is not allowed", email))
-		return
-	}
-	minted, err := h.rt.NewIdentity("unblessed")
+	// TODO(ashankar): Render a nice HTML page instead with the browser's timezone
+	// and all that good stuff. Will do that in a subsequent change.
+	ch, err := auditor.ReadAuditLog(h.auditor, email)
 	if err != nil {
-		util.HTTPServerError(w, fmt.Errorf("Failed to mint new identity: %v", err))
+		vlog.Errorf("Unable to read audit log: %v", err)
+		util.HTTPServerError(w, fmt.Errorf("unable to read audit log"))
 		return
 	}
-	blessing, err := h.rt.Identity().Bless(minted.PublicID(), email, 24*time.Hour*time.Duration(h.minExpiryDays), nil)
-	if err != nil {
-		util.HTTPServerError(w, fmt.Errorf("%v.Bless(%q, %q, %d days, nil) failed: %v", h.rt.Identity(), minted, h.minExpiryDays, err))
-		return
+	w.Header().Set("Context-Type", "text/plain")
+	for entry := range ch {
+		w.Write([]byte(fmt.Sprintf("%v\n", entry)))
 	}
-	blessed, err := minted.Derive(blessing)
-	if err != nil {
-		util.HTTPServerError(w, fmt.Errorf("%v.Derive(%q) failed: %v", minted, blessed, err))
-		return
-	}
-	vlog.Infof("Created new identity: %v", blessed)
-	util.HTTPSend(w, blessed)
 }
 
 // ExchangeAuthCodeForEmail exchanges the authorization code (which must
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index fd91182..0a1d716 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -13,12 +13,15 @@
 
 	"veyron/lib/signals"
 	vsecurity "veyron/security"
+	"veyron/security/audit"
+	"veyron/services/identity/auditor"
 	"veyron/services/identity/blesser"
 	"veyron/services/identity/googleoauth"
 	"veyron/services/identity/handlers"
 
 	"veyron2"
 	"veyron2/ipc"
+	"veyron2/naming"
 	"veyron2/rt"
 	"veyron2/security"
 	"veyron2/vlog"
@@ -33,19 +36,25 @@
 	host          = flag.String("host", defaultHost(), "Hostname the HTTP server listens on. This can be the name of the host running the webserver, but if running behind a NAT or load balancer, this should be the host name that clients will connect to. For example, if set to 'x.com', Veyron identities will have the IssuerName set to 'x.com' and clients can expect to find the public key of the signer at 'x.com/pubkey/'.")
 	minExpiryDays = flag.Int("min_expiry_days", 365, "Minimum expiry time (in days) of identities issued by this server")
 
+	auditprefix = flag.String("audit", "", "File prefix to files where auditing information will be written.")
+	auditfilter = flag.String("audit_filter", "", "If non-empty, instead of starting the server the audit log will be dumped to STDOUT (with the filter set to the value of this flag. '/' can be used to dump all events).")
+
 	// Configuration for various Google OAuth-based clients.
-	googleConfigWeb       = flag.String("google_config_web", "", "Path to JSON-encoded OAuth client configuration for the web application for generating PrivateIDs (Use the 'Download JSON' link on the Google APIs console).")
+	googleConfigWeb       = flag.String("google_config_web", "", "Path to JSON-encoded OAuth client configuration for the web application that renders the audit log for blessings provided by this provider.")
 	googleConfigInstalled = flag.String("google_config_installed", "", "Path to the JSON-encoded OAuth client configuration for installed client applications that obtain blessings (via the OAuthBlesser.BlessUsingAuthorizationCode RPC) from this server (like the 'identity' command like tool and its 'seekblessing' command.")
 	googleConfigChrome    = flag.String("google_config_chrome", "", "Path to the JSON-encoded OAuth client configuration for Chrome browser applications that obtain blessings from this server (via the OAuthBlesser.BlessUsingAccessToken RPC) from this server.")
 	googleDomain          = flag.String("google_domain", "", "An optional domain name. When set, only email addresses from this domain are allowed to authenticate via Google OAuth")
 )
 
 func main() {
-	// Setup flags and logging
 	flag.Usage = usage
-	r := rt.Init()
+	r := rt.Init(providerIdentity())
 	defer r.Cleanup()
 
+	if len(*auditfilter) > 0 {
+		dumpAuditLog()
+		return
+	}
 	// Setup handlers
 	http.Handle("/pubkey/", handlers.Object{r.Identity().PublicID().PublicKey()}) // public key of this identity server
 	if enableRandomHandler() {
@@ -54,22 +63,20 @@
 	http.HandleFunc("/bless/", handlers.Bless) // use a provided PrivateID to bless a provided PublicID
 
 	// Google OAuth
-	ipcServer, err := setupGoogleBlessingServer(r)
+	ipcServer, ipcServerEP, err := setupGoogleBlessingServer(r)
 	if err != nil {
 		vlog.Fatalf("Failed to setup veyron services for blessing: %v", err)
 	}
 	if ipcServer != nil {
 		defer ipcServer.Stop()
 	}
-	if enabled, clientID, clientSecret := enableGoogleOAuth(*googleConfigWeb); enabled {
+	if enabled, clientID, clientSecret := enableGoogleOAuth(*googleConfigWeb); enabled && len(*auditprefix) > 0 {
 		n := "/google/"
 		http.Handle(n, googleoauth.NewHandler(googleoauth.HandlerArgs{
-			Addr:                fmt.Sprintf("%s%s", httpaddress(), n),
-			ClientID:            clientID,
-			ClientSecret:        clientSecret,
-			MinExpiryDays:       *minExpiryDays,
-			Runtime:             r,
-			RestrictEmailDomain: *googleDomain,
+			Addr:         fmt.Sprintf("%s%s", httpaddress(), n),
+			ClientID:     clientID,
+			ClientSecret: clientSecret,
+			Auditor:      *auditprefix,
 		}))
 	}
 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@@ -77,10 +84,16 @@
 		if ipcServer != nil {
 			servers, _ = ipcServer.Published()
 		}
+		if len(servers) == 0 {
+			// No addresses published, publish the endpoint instead (which may not be usable everywhere, but oh-well).
+			servers = append(servers, naming.JoinAddressName(ipcServerEP.String(), ""))
+		}
 		args := struct {
+			Self                 string
 			GoogleWeb, RandomWeb bool
 			GoogleServers        []string
 		}{
+			Self:          rt.R().Identity().PublicID().Names()[0],
 			GoogleWeb:     len(*googleConfigWeb) > 0,
 			RandomWeb:     enableRandomHandler(),
 			GoogleServers: servers,
@@ -94,7 +107,7 @@
 	<-signals.ShutdownOnSignals()
 }
 
-func setupGoogleBlessingServer(r veyron2.Runtime) (ipc.Server, error) {
+func setupGoogleBlessingServer(r veyron2.Runtime) (ipc.Server, naming.Endpoint, error) {
 	var enable bool
 	params := blesser.GoogleParams{
 		R:                 r,
@@ -111,27 +124,29 @@
 		params.AccessTokenClient.ID = clientID
 	}
 	if !enable {
-		return nil, nil
+		return nil, nil, nil
 	}
 	server, err := r.NewServer()
 	if err != nil {
-		return nil, fmt.Errorf("failed to create new ipc.Server: %v", err)
+		return nil, nil, fmt.Errorf("failed to create new ipc.Server: %v", err)
 	}
 	ep, err := server.Listen(*protocol, *address)
 	if err != nil {
-		return nil, fmt.Errorf("server.Listen(%q, %q) failed: %v", "tcp", *address, err)
+		return nil, nil, fmt.Errorf("server.Listen(%q, %q) failed: %v", "tcp", *address, err)
 	}
 	allowEveryoneACL := security.ACL{security.AllPrincipals: security.AllLabels}
 	objectname := fmt.Sprintf("identity/%s/google", r.Identity().PublicID().Names()[0])
 	if err := server.Serve(objectname, ipc.SoloDispatcher(blesser.NewGoogleOAuthBlesserServer(params), vsecurity.NewACLAuthorizer(allowEveryoneACL))); err != nil {
-		return nil, fmt.Errorf("failed to start Veyron service: %v", err)
+		return nil, nil, fmt.Errorf("failed to start Veyron service: %v", err)
 	}
 	vlog.Infof("Google blessing service enabled at endpoint %v and name %q", ep, objectname)
-	return server, nil
+	return server, ep, nil
 }
 
-func enableTLS() bool           { return len(*tlsconfig) > 0 }
-func enableRandomHandler() bool { return len(*googleConfigInstalled)+len(*googleConfigWeb) == 0 }
+func enableTLS() bool { return len(*tlsconfig) > 0 }
+func enableRandomHandler() bool {
+	return len(*googleConfigInstalled)+len(*googleConfigWeb)+len(*googleConfigChrome) == 0
+}
 func enableGoogleOAuth(config string) (enabled bool, clientID, clientSecret string) {
 	fname := config
 	if len(fname) == 0 {
@@ -196,6 +211,26 @@
 	return host
 }
 
+// providerIdentity returns the identity of the identity provider (i.e., this program) itself.
+func providerIdentity() veyron2.ROpt {
+	// TODO(ashankar): This scheme of initializing a runtime just to share the "load identity" code is ridiculous.
+	// Figure out a way to update the runtime's identity with a wrapper and avoid this spurios "New" call.
+	r, err := rt.New()
+	if err != nil {
+		vlog.Fatal(err)
+	}
+	defer r.Cleanup()
+	id := r.Identity()
+	if len(*auditprefix) > 0 {
+		auditor, err := auditor.NewFileAuditor(*auditprefix)
+		if err != nil {
+			vlog.Fatal(err)
+		}
+		id = audit.NewPrivateID(id, auditor)
+	}
+	return veyron2.RuntimeID(id)
+}
+
 func httpaddress() string {
 	_, port, err := net.SplitHostPort(*httpaddr)
 	if err != nil {
@@ -208,6 +243,21 @@
 	return fmt.Sprintf("%s://%s:%v", scheme, *host, port)
 }
 
+func dumpAuditLog() {
+	if len(*auditprefix) == 0 {
+		vlog.Fatalf("Must set --audit")
+	}
+	ch, err := auditor.ReadAuditLog(*auditprefix, *auditfilter)
+	if err != nil {
+		vlog.Fatal(err)
+	}
+	idx := 0
+	for entry := range ch {
+		fmt.Printf("%6d) %v\n", idx, entry)
+		idx++
+	}
+}
+
 var tmpl = template.Must(template.New("main").Parse(`<!doctype html>
 <html>
 <head>
@@ -218,27 +268,34 @@
 </head>
 <body>
 <div class="container">
-<div class="page-header"><h1>Veyron Identity Generation</h1></div>
+<div class="page-header"><h2>{{.Self}}</h2><h4>A Veyron Identity Provider</h4></div>
 <div class="well">
-This HTTP server mints veyron identities. The public key of the identity of this server is available in
-<a class="btn btn-xs btn-info" href="/pubkey/base64vom">base64-encoded-vom-encoded</a> format.
+This is a Veyron identity provider that provides blessings with the name prefix <mark>{{.Self}}</mark>. The public
+key of this provider is available in <a class="btn btn-xs btn-primary" href="/pubkey/base64vom">base64-encoded-vom-encoded</a> format.
 </div>
+
 {{if .GoogleServers}}
 <div class="well">
-The Veyron service for blessing is published at:
-<tt>
-{{range .GoogleServers}}{{.}}{{end}}
-</tt>
+Blessings are provided via Veyron RPCs to: <tt>{{range .GoogleServers}}{{.}}{{end}}</tt>
 </div>
 {{end}}
 
 {{if .GoogleWeb}}
-<a class="btn btn-lg btn-primary" href="/google/auth">Google</a>
+<div class="well">
+This page provides the ability to <a class="btn btn-xs btn-primary" href="/google/auth">enumerate</a> blessings provided with your
+email address as the name.
+</div>
 {{end}}
+
 {{if .RandomWeb}}
-<a class="btn btn-lg btn-primary" href="/random/">Random</a>
+<div class="well">
+You can obtain a randomly assigned PrivateID <a class="btn btn-sm btn-primary" href="/random/">here</a>
+</div>
 {{end}}
-<a class="btn btn-lg btn-primary" href="/bless/">Bless As</a>
+
+<div class="well">
+You can use <a class="btn btn-xs btn-primary" href="/bless/">this form</a> to offload crypto for blessing to this HTTP server
+</div>
 
 </div>
 </body>