veyron/services/identity: Implemented sql auditor.

* This has been successfully tested with a local mysql server instance.
* Before deployment a cloudsql instance will have to be created and the
  connection information will need to be in a json file and passed in via
  the audit_config flag.

Change-Id: I3c948f758890c06bbd8233e0193e1729ec6bfd29
diff --git a/security/audit/principal.go b/security/audit/principal.go
index c7c2e50..efd1323 100644
--- a/security/audit/principal.go
+++ b/security/audit/principal.go
@@ -79,7 +79,10 @@
 
 func addCaveats(args args, caveats ...security.Caveat) args {
 	for _, c := range caveats {
-		args = append(args, c)
+		// TODO(ashankar,suharshs): Should isUnconstrainedCaveat in veyron2/security be exported and used here?
+		if len(c.ValidatorVOM) > 0 {
+			args = append(args, c)
+		}
 	}
 	return args
 }
diff --git a/security/util.go b/security/util.go
index 6e1f848..e7ed33f 100644
--- a/security/util.go
+++ b/security/util.go
@@ -123,6 +123,8 @@
 //
 // It is an error if any of the provided caveat bytes cannot
 // be decoded into a security.CaveatValidator.
+// TODO(suharshs,ashankar,ataly): Rather than quitting on non-decodable caveats, just skip
+// them and return on caveats that we can decode.
 func CaveatValidators(caveats ...security.Caveat) ([]security.CaveatValidator, error) {
 	if len(caveats) == 0 {
 		return nil, nil
diff --git a/services/identity/auditor/blessing_auditor.go b/services/identity/auditor/blessing_auditor.go
new file mode 100644
index 0000000..a76bc89
--- /dev/null
+++ b/services/identity/auditor/blessing_auditor.go
@@ -0,0 +1,147 @@
+package auditor
+
+import (
+	"bytes"
+	"fmt"
+	_ "github.com/go-sql-driver/mysql"
+	"strings"
+	"time"
+
+	vsecurity "veyron.io/veyron/veyron/security"
+	"veyron.io/veyron/veyron/security/audit"
+	"veyron.io/veyron/veyron2/security"
+	"veyron.io/veyron/veyron2/vlog"
+	"veyron.io/veyron/veyron2/vom"
+)
+
+// BlessingLogReader provides the Read method to read audit logs.
+// Read returns a channel of BlessingEntrys whose extension matches the provided email.
+type BlessingLogReader interface {
+	Read(email string) <-chan BlessingEntry
+}
+
+// BlessingEntry contains important logged information about a blessed principal.
+type BlessingEntry struct {
+	Email              string
+	Caveats            []security.Caveat
+	Timestamp          time.Time // Time when the blesings were created.
+	RevocationCaveatID string
+	Blessings          security.Blessings
+}
+
+// NewSQLBlessingAuditor returns an auditor for wrapping a principal with, and a BlessingLogReader
+// for reading the audits made by that auditor. The config is used to construct the connection
+// to the SQL database that the auditor and BlessingLogReader use.
+func NewSQLBlessingAuditor(config SQLConfig) (audit.Auditor, BlessingLogReader, error) {
+	db, err := newSQLDatabase(config)
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to create sql db: %v", err)
+	}
+	auditor, reader := &blessingAuditor{db}, &blessingLogReader{db}
+	return auditor, reader, nil
+}
+
+type blessingAuditor struct {
+	db database
+}
+
+func (a *blessingAuditor) Audit(entry audit.Entry) error {
+	if entry.Method != "Bless" {
+		return nil
+	}
+	dbentry, err := newDatabaseEntry(entry)
+	if err != nil {
+		return err
+	}
+	return a.db.Insert(dbentry)
+}
+
+type blessingLogReader struct {
+	db database
+}
+
+func (r *blessingLogReader) Read(email string) <-chan BlessingEntry {
+	c := make(chan BlessingEntry)
+	go r.sendAuditEvents(c, email)
+	return c
+}
+
+func (r *blessingLogReader) sendAuditEvents(dst chan<- BlessingEntry, email string) {
+	defer close(dst)
+	dbch := r.db.Query(email)
+	for dbentry := range dbch {
+		var entry BlessingEntry
+		if err := entry.fromDatabaseEntry(dbentry); err != nil {
+			vlog.Errorf("Corrupt database data? %#v, %v", dbentry, err)
+			continue
+		}
+		dst <- entry
+	}
+}
+
+func newDatabaseEntry(entry audit.Entry) (databaseEntry, error) {
+	d := databaseEntry{timestamp: entry.Timestamp}
+	extension, ok := entry.Arguments[2].(string)
+	if !ok {
+		return d, fmt.Errorf("failed to extract extension")
+	}
+	d.email = strings.Split(extension, "/")[0]
+	var caveats []security.Caveat
+	for _, arg := range entry.Arguments[3:] {
+		if cav, ok := arg.(security.Caveat); !ok {
+			return d, fmt.Errorf("failed to extract Caveat")
+		} else {
+			caveats = append(caveats, cav)
+		}
+	}
+	var blessings security.Blessings
+	if blessings, ok = entry.Results[0].(security.Blessings); !ok {
+		return d, fmt.Errorf("failed to extract result blessing")
+	}
+	{
+		var buf bytes.Buffer
+		if err := vom.NewEncoder(&buf).Encode(security.MarshalBlessings(blessings)); err != nil {
+			return d, err
+		}
+		d.blessings = buf.Bytes()
+	}
+	{
+		var buf bytes.Buffer
+		if err := vom.NewEncoder(&buf).Encode(caveats); err != nil {
+			return d, err
+		}
+		d.caveats = buf.Bytes()
+	}
+	return d, nil
+}
+
+func (b *BlessingEntry) fromDatabaseEntry(dbentry databaseEntry) error {
+	b.Email = dbentry.email
+	b.Timestamp = dbentry.timestamp
+	var wireBlessings security.WireBlessings
+	var err error
+	if err := vom.NewDecoder(bytes.NewBuffer(dbentry.blessings)).Decode(&wireBlessings); err != nil {
+		return err
+	}
+	if b.Blessings, err = security.NewBlessings(wireBlessings); err != nil {
+		return err
+	}
+	if err := vom.NewDecoder(bytes.NewBuffer(dbentry.caveats)).Decode(&b.Caveats); err != nil {
+		return err
+	}
+	b.RevocationCaveatID, err = revocationCaveatID(b.Caveats)
+	return err
+}
+
+func revocationCaveatID(caveats []security.Caveat) (string, error) {
+	validators, err := vsecurity.CaveatValidators(caveats...)
+	if err != nil {
+		return "", err
+	}
+	for _, cav := range validators {
+		if tpcav, ok := cav.(security.ThirdPartyCaveat); ok {
+			return tpcav.ID(), nil
+		}
+	}
+	return "", nil
+}
diff --git a/services/identity/auditor/blessing_auditor_test.go b/services/identity/auditor/blessing_auditor_test.go
new file mode 100644
index 0000000..946d976
--- /dev/null
+++ b/services/identity/auditor/blessing_auditor_test.go
@@ -0,0 +1,128 @@
+package auditor
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	vsecurity "veyron.io/veyron/veyron/security"
+	"veyron.io/veyron/veyron/security/audit"
+	"veyron.io/veyron/veyron2/security"
+)
+
+func TestBlessingAuditor(t *testing.T) {
+	db := &mockDatabase{}
+	auditor, reader := &blessingAuditor{db}, &blessingLogReader{db}
+
+	p, err := vsecurity.NewPrincipal()
+	if err != nil {
+		t.Fatalf("failed to create principal: %v", err)
+	}
+	expiryCaveat := newCaveat(security.ExpiryCaveat(time.Now().Add(time.Hour)))
+	revocationCaveat := newThirdPartyCaveat(t, p)
+
+	tests := []struct {
+		Extension          string
+		Email              string
+		Caveats            []security.Caveat
+		RevocationCaveatID string
+		Blessings          security.Blessings
+	}{
+		{
+			Extension:          "email/nocaveats",
+			Email:              "email",
+			RevocationCaveatID: "",
+			Blessings:          newBlessing(t, p, "test/email/nocaveats"),
+		},
+		{
+			Extension:          "email/caveat",
+			Email:              "email",
+			Caveats:            []security.Caveat{expiryCaveat},
+			RevocationCaveatID: "",
+			Blessings:          newBlessing(t, p, "test/email/caveat"),
+		},
+		{
+			Extension:          "email/caveatAndRevocation",
+			Email:              "email",
+			Caveats:            []security.Caveat{expiryCaveat, newCaveat(security.NewCaveat(revocationCaveat))},
+			RevocationCaveatID: revocationCaveat.ID(),
+			Blessings:          newBlessing(t, p, "test/email/caveatAndRevocation"),
+		},
+	}
+
+	for _, test := range tests {
+		args := []interface{}{nil, nil, test.Extension}
+		for _, cav := range test.Caveats {
+			args = append(args, cav)
+		}
+		if err := auditor.Audit(audit.Entry{
+			Method:    "Bless",
+			Arguments: args,
+			Results:   []interface{}{test.Blessings},
+		}); err != nil {
+			t.Errorf("Failed to audit Blessing %v: %v", test.Blessings, err)
+		}
+		ch := reader.Read("query")
+		got := <-ch
+		if got.Email != test.Email {
+			t.Errorf("got %v, want %v", got.Email, test.Email)
+		}
+		if !reflect.DeepEqual(got.Caveats, test.Caveats) {
+			t.Errorf("got %#v, want %#v", got.Caveats, test.Caveats)
+		}
+		if got.RevocationCaveatID != test.RevocationCaveatID {
+			t.Errorf("got %v, want %v", got.RevocationCaveatID, test.RevocationCaveatID)
+		}
+		if !reflect.DeepEqual(got.Blessings, test.Blessings) {
+			t.Errorf("got %v, want %v", got.Blessings, test.Blessings)
+		}
+		var extraRoutines bool
+		for _ = range ch {
+			// Drain the channel to prevent the producer goroutines from being leaked.
+			extraRoutines = true
+		}
+		if extraRoutines {
+			t.Errorf("Got more entries that expected for test %+v", test)
+		}
+	}
+}
+
+type mockDatabase struct {
+	NextEntry databaseEntry
+}
+
+func (db *mockDatabase) Insert(entry databaseEntry) error {
+	db.NextEntry = entry
+	return nil
+}
+func (db *mockDatabase) Query(email string) <-chan databaseEntry {
+	c := make(chan databaseEntry)
+	go func() {
+		c <- db.NextEntry
+		close(c)
+	}()
+	return c
+}
+
+func newThirdPartyCaveat(t *testing.T, p security.Principal) security.ThirdPartyCaveat {
+	tp, err := security.NewPublicKeyCaveat(p.PublicKey(), "location", security.ThirdPartyRequirements{}, newCaveat(security.MethodCaveat("method")))
+	if err != nil {
+		t.Fatal(err)
+	}
+	return tp
+}
+
+func newBlessing(t *testing.T, p security.Principal, name string) security.Blessings {
+	b, err := p.BlessSelf(name)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return b
+}
+
+func newCaveat(caveat security.Caveat, err error) security.Caveat {
+	if err != nil {
+		panic(err)
+	}
+	return caveat
+}
diff --git a/services/identity/auditor/sql_database.go b/services/identity/auditor/sql_database.go
new file mode 100644
index 0000000..7b601b6
--- /dev/null
+++ b/services/identity/auditor/sql_database.go
@@ -0,0 +1,85 @@
+package auditor
+
+import (
+	"database/sql"
+	"fmt"
+	_ "github.com/go-sql-driver/mysql"
+
+	"time"
+	"veyron.io/veyron/veyron2/vlog"
+)
+
+// SQLConfig contains the information to create a connection to a sql database.
+type SQLConfig struct {
+	// Database is a driver specific string specifying how to connect to the database.
+	Database string `json:"database"`
+	Table    string `json:"table"`
+}
+
+type database interface {
+	Insert(entry databaseEntry) error
+	Query(email string) <-chan databaseEntry
+}
+
+type databaseEntry struct {
+	email, revocationCaveatID string
+	caveats, blessings        []byte
+	timestamp                 time.Time
+}
+
+// newSQLDatabase returns a SQL implementation of the database interface.
+// If the table does not exist it creates it.
+func newSQLDatabase(config SQLConfig) (database, error) {
+	db, err := sql.Open("mysql", config.Database)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create database with config(%v): %v", config, err)
+	}
+	if err := db.Ping(); err != nil {
+		return nil, err
+	}
+	createStmt, err := db.Prepare(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s ( Email NVARCHAR(256), Caveats BLOB, Timestamp DATETIME, RevocationCaveatID NVARCHAR(1000), Blessings BLOB );", config.Table))
+	if err != nil {
+		return nil, err
+	}
+	if _, err = createStmt.Exec(); err != nil {
+		return nil, err
+	}
+	insertStmt, err := db.Prepare(fmt.Sprintf("INSERT INTO %s (Email, Caveats, RevocationCaveatID, Timestamp, Blessings) VALUES (?, ?, ?, ?, ?)", config.Table))
+	if err != nil {
+		return nil, err
+	}
+	queryStmt, err := db.Prepare(fmt.Sprintf("SELECT Email, Caveats, RevocationCaveatID, Timestamp, Blessings from %s WHERE Email=?", config.Table))
+	return sqlDatabase{insertStmt, queryStmt}, err
+}
+
+type sqlDatabase struct {
+	insertStmt, queryStmt *sql.Stmt
+}
+
+func (s sqlDatabase) Insert(entry databaseEntry) error {
+	_, err := s.insertStmt.Exec(entry.email, entry.caveats, entry.revocationCaveatID, entry.timestamp, entry.blessings)
+	return err
+}
+
+func (s sqlDatabase) Query(email string) <-chan databaseEntry {
+	c := make(chan databaseEntry)
+	go s.sendDatabaseEntries(email, c)
+	return c
+}
+
+func (s sqlDatabase) sendDatabaseEntries(email string, dst chan<- databaseEntry) {
+	defer close(dst)
+	rows, err := s.queryStmt.Query(email)
+	if err != nil {
+		vlog.Errorf("query failed %v", err)
+		return
+	}
+	for rows.Next() {
+		var dbentry databaseEntry
+		if err = rows.Scan(&dbentry.email, &dbentry.caveats, &dbentry.revocationCaveatID, &dbentry.timestamp, &dbentry.blessings); err != nil {
+			vlog.Errorf("scan of row failed %v", err)
+			return
+		}
+		dst <- dbentry
+	}
+}
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 11dea0b..255c9b1 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -39,6 +39,7 @@
 
 	"code.google.com/p/goauth2/oauth"
 
+	"veyron.io/veyron/veyron/services/identity/auditor"
 	"veyron.io/veyron/veyron/services/identity/blesser"
 	"veyron.io/veyron/veyron/services/identity/revocation"
 	"veyron.io/veyron/veyron/services/identity/util"
@@ -73,9 +74,8 @@
 	// client_id and client_secret registered with the Google Developer
 	// Console for API access.
 	ClientID, ClientSecret string
-	// Prefix for the audit log from which data will be sourced.
-	// (auditor.ReadAuditLog).
-	Auditor string
+	// BlessingLogReder is needed for reading audit logs.
+	BlessingLogReader auditor.BlessingLogReader
 	// The RevocationManager is used to revoke blessings granted with a revocation caveat.
 	// If nil, then revocation caveats cannot be added to blessings and an expiration caveat
 	// will be used instead.
@@ -171,6 +171,24 @@
 		util.HTTPBadRequest(w, r, err)
 		return
 	}
+
+	type tmplentry struct {
+		Timestamp      time.Time
+		Caveats        []security.Caveat
+		RevocationTime time.Time
+		Blessed        security.Blessings
+		Token          string
+	}
+	tmplargs := struct {
+		Log                chan tmplentry
+		Email, RevokeRoute string
+	}{
+		Log:         make(chan tmplentry),
+		Email:       email,
+		RevokeRoute: revokeRoute,
+	}
+	entrych := h.args.BlessingLogReader.Read(email)
+
 	w.Header().Set("Context-Type", "text/html")
 	// This MaybeSetCookie call is needed to ensure that a cookie is created. Since the
 	// header cannot be changed once the body is written to, this needs to be called first.
@@ -179,16 +197,31 @@
 		util.HTTPServerError(w, err)
 		return
 	}
-	w.Write([]byte(fmt.Sprintf(`
-<html>
-<head>
-  <title>DISABLED FUNCTIONALITY</title>
- </head>
- <body>
- <h1>Attention %s</h1>
- <h2>This functionality has been temporarily disabled. ashankar@ and suharshs@ will know more</h2>
- </body>
- </html>`, email)))
+	go func(ch chan tmplentry) {
+		defer close(ch)
+		for entry := range entrych {
+			tmplEntry := tmplentry{
+				Timestamp: entry.Timestamp,
+				Caveats:   entry.Caveats,
+				Blessed:   entry.Blessings,
+			}
+			if len(entry.RevocationCaveatID) > 0 && h.args.RevocationManager != nil {
+				if revocationTime := h.args.RevocationManager.GetRevocationTime(entry.RevocationCaveatID); revocationTime != nil {
+					tmplEntry.RevocationTime = *revocationTime
+				} else {
+					caveatID := base64.URLEncoding.EncodeToString([]byte(entry.RevocationCaveatID))
+					if tmplEntry.Token, err = h.csrfCop.NewToken(w, r, clientIDCookie, caveatID); err != nil {
+						vlog.Errorf("Failed to create CSRF token[%v] for request %#v", err, r)
+					}
+				}
+			}
+			ch <- tmplEntry
+		}
+	}(tmplargs.Log)
+	if err := tmplViewBlessings.Execute(w, tmplargs); err != nil {
+		vlog.Errorf("Unable to execute audit page template: %v", err)
+		util.HTTPServerError(w, err)
+	}
 }
 
 func (h *handler) revoke(w http.ResponseWriter, r *http.Request) {
diff --git a/services/identity/googleoauth/template.go b/services/identity/googleoauth/template.go
index a01f1e1..6bac1f7 100644
--- a/services/identity/googleoauth/template.go
+++ b/services/identity/googleoauth/template.go
@@ -73,22 +73,24 @@
 <table class="table table-bordered table-hover table-responsive">
 <thead>
 <tr>
-  <th>Blessing sought as</th>
   <th>Blessed as</th>
+  <th>Public Key</th>
   <th>Issued</th>
-  <th>Expires</th>
-  <th>PublicKey</th>
+  <th>Caveats</th>
   <th>Revoked</th>
   </tr>
 </thead>
 <tbody>
 {{range .Log}}
 <tr>
-<td>{{.Blessee}}</td>
 <td>{{.Blessed}}</td>
-<td><div class="unixtime" data-unixtime={{.Start.Unix}}>{{.Start.String}}</div></td>
-<td><div class="unixtime" data-unixtime={{.End.Unix}}>{{.End.String}}</div></td>
-<td>{{.Blessee.PublicKey}}</td>
+<td>{{.Blessed.PublicKey}}</td>
+<td><div class="unixtime" data-unixtime={{.Timestamp.Unix}}>{{.Timestamp.String}}</div></td>
+<td>
+{{range .Caveats}}
+  {{.}}</br>
+{{end}}
+</td>
 <td>
   {{ if .Token }}
   <button class="revoke" value="{{.Token}}">Revoke</button>
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index 6665804..84a2bac 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -3,9 +3,11 @@
 
 import (
 	"crypto/rand"
+	"encoding/json"
 	"flag"
 	"fmt"
 	"html/template"
+	"io/ioutil"
 	"net"
 	"net/http"
 	"os"
@@ -22,6 +24,8 @@
 	"veyron.io/veyron/veyron2/vlog"
 
 	"veyron.io/veyron/veyron/lib/signals"
+	"veyron.io/veyron/veyron/security/audit"
+	"veyron.io/veyron/veyron/services/identity/auditor"
 	"veyron.io/veyron/veyron/services/identity/blesser"
 	"veyron.io/veyron/veyron/services/identity/googleoauth"
 	"veyron.io/veyron/veyron/services/identity/handlers"
@@ -38,9 +42,8 @@
 	tlsconfig = flag.String("tlsconfig", "", "Comma-separated list of TLS certificate and private key files. This must be provided.")
 	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/'.")
 
-	// Flags controlling auditing of Blessing operations.
-	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).")
+	// Flag controlling auditing of Blessing operations.
+	auditConfig = flag.String("audit_config", "", "A JSON-encoded file with sql server configuration information for auditing. The file must have an entry for user, host, password, database, and table.")
 
 	// Configuration for various Google OAuth-based clients.
 	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.")
@@ -61,14 +64,10 @@
 
 func main() {
 	flag.Usage = usage
-	r := rt.Init(providerPrincipal())
+	p, blessingLogReader := providerPrincipal()
+	r := rt.Init(options.RuntimePrincipal{p})
 	defer r.Cleanup()
 
-	if len(*auditfilter) > 0 {
-		dumpAuditLog()
-		return
-	}
-
 	// Calling with empty string returns a empty RevocationManager
 	revocationManager, err := revocation.NewRevocationManager(*revocationDir)
 	if err != nil {
@@ -95,7 +94,7 @@
 			Addr:                    fmt.Sprintf("%s%s", httpaddress(), n),
 			ClientID:                clientID,
 			ClientSecret:            clientSecret,
-			Auditor:                 *auditprefix,
+			BlessingLogReader:       blessingLogReader,
 			RevocationManager:       revocationManager,
 			MacaroonBlessingService: naming.JoinAddressName(published[0], macaroonService),
 		})
@@ -120,7 +119,7 @@
 		if len(*googleConfigChrome) > 0 || len(*googleConfigAndroid) > 0 {
 			args.GoogleServers = appendSuffixTo(published, googleService)
 		}
-		if len(*auditprefix) > 0 && len(*googleConfigWeb) > 0 {
+		if len(*auditConfig) > 0 && len(*googleConfigWeb) > 0 {
 			args.ListBlessingsRoute = googleoauth.ListBlessingsRoute
 		}
 		if err := tmpl.Execute(w, args); err != nil {
@@ -280,8 +279,9 @@
 	return host
 }
 
-// providerPrincipal returns the Principal to use for the identity provider (i.e., this program).
-func providerPrincipal() veyron2.ROpt {
+// providerPrincipal returns the Principal to use for the identity provider (i.e., this program) and
+// the database where audits will be store. If no database exists nil will be returned.
+func providerPrincipal() (security.Principal, auditor.BlessingLogReader) {
 	// TODO(ashankar): Somewhat silly to have to create a runtime, but oh-well.
 	r, err := rt.New()
 	if err != nil {
@@ -289,12 +289,33 @@
 	}
 	defer r.Cleanup()
 	p := r.Principal()
-	// TODO(ashankar): Hook this up with Suharsh's new auditor implementation.
-	if len(*auditprefix) == 0 {
-		return options.RuntimePrincipal{p}
+	if len(*auditConfig) == 0 {
+		return p, nil
 	}
-	vlog.Fatalf("--auditprefix is not supported just yet!")
-	return nil
+	config, err := readSQLConfigFromFile(*auditConfig)
+	if err != nil {
+		vlog.Fatalf("Failed to read sql config: %v", err)
+	}
+	auditor, reader, err := auditor.NewSQLBlessingAuditor(config)
+	if err != nil {
+		vlog.Fatalf("Failed to create sql auditor from config: %v", err)
+	}
+	return audit.NewPrincipal(p, auditor), reader
+}
+
+func readSQLConfigFromFile(file string) (auditor.SQLConfig, error) {
+	var config auditor.SQLConfig
+	content, err := ioutil.ReadFile(file)
+	if err != nil {
+		return config, err
+	}
+	if err := json.Unmarshal(content, &config); err != nil {
+		return config, err
+	}
+	if len(strings.Split(config.Table, " ")) != 1 || strings.Contains(config.Table, ";") {
+		return config, fmt.Errorf("sql config table value must be 1 word long")
+	}
+	return config, nil
 }
 
 func httpaddress() string {
@@ -305,13 +326,6 @@
 	return fmt.Sprintf("https://%s:%v", *host, port)
 }
 
-func dumpAuditLog() {
-	if len(*auditprefix) == 0 {
-		vlog.Fatalf("Must set --audit")
-	}
-	vlog.Fatalf("Auditing support disabled. Please contact ashankar@ or suharshs@ for restoration timeline")
-}
-
 var tmpl = template.Must(template.New("main").Parse(`<!doctype html>
 <html>
 <head>