veyron/services/identity: Implemented sql revocation manager and added
sql database tests.
* 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 revocation_config flag.
Change-Id: I3dfae895d82ca8ff8837bcae4a0e146c97622372
diff --git a/services/identity/auditor/blessing_auditor.go b/services/identity/auditor/blessing_auditor.go
index 28f7b62..b00d28d 100644
--- a/services/identity/auditor/blessing_auditor.go
+++ b/services/identity/auditor/blessing_auditor.go
@@ -2,8 +2,8 @@
import (
"bytes"
+ "database/sql"
"fmt"
- _ "github.com/go-sql-driver/mysql"
"strings"
"time"
@@ -32,8 +32,8 @@
// 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)
+func NewSQLBlessingAuditor(sqlDB *sql.DB) (audit.Auditor, BlessingLogReader, error) {
+ db, err := newSQLDatabase(sqlDB, "BlessingAudit")
if err != nil {
return nil, nil, fmt.Errorf("failed to create sql db: %v", err)
}
diff --git a/services/identity/auditor/blessing_auditor_test.go b/services/identity/auditor/blessing_auditor_test.go
index 946d976..7970ae3 100644
--- a/services/identity/auditor/blessing_auditor_test.go
+++ b/services/identity/auditor/blessing_auditor_test.go
@@ -76,12 +76,12 @@
if !reflect.DeepEqual(got.Blessings, test.Blessings) {
t.Errorf("got %v, want %v", got.Blessings, test.Blessings)
}
- var extraRoutines bool
+ var extra bool
for _ = range ch {
// Drain the channel to prevent the producer goroutines from being leaked.
- extraRoutines = true
+ extra = true
}
- if extraRoutines {
+ if extra {
t.Errorf("Got more entries that expected for test %+v", test)
}
}
diff --git a/services/identity/auditor/sql_database.go b/services/identity/auditor/sql_database.go
index 405f97f..aeb9af6 100644
--- a/services/identity/auditor/sql_database.go
+++ b/services/identity/auditor/sql_database.go
@@ -4,61 +4,52 @@
"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
- decodeErr error
+ email string
+ caveats, blessings []byte
+ timestamp time.Time
+ decodeErr error
}
// 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))
+func newSQLDatabase(db *sql.DB, table string) (database, error) {
+ createStmt, err := db.Prepare(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s ( Email NVARCHAR(256), Caveats BLOB, Timestamp DATETIME, Blessings BLOB );", 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))
+ insertStmt, err := db.Prepare(fmt.Sprintf("INSERT INTO %s (Email, Caveats, Timestamp, Blessings) VALUES (?, ?, ?, ?)", 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))
+ queryStmt, err := db.Prepare(fmt.Sprintf("SELECT Email, Caveats, Timestamp, Blessings FROM %s WHERE Email=?", table))
return sqlDatabase{insertStmt, queryStmt}, err
}
+// Table with 4 columns:
+// (1) Email = string email of the Blessee.
+// (2) Caveats = vom encoded caveats
+// (3) Blessings = vom encoded resulting blessings.
+// (4) Timestamp = time that the blessing happened.
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)
+ _, err := s.insertStmt.Exec(entry.email, entry.caveats, entry.timestamp, entry.blessings)
return err
}
@@ -78,7 +69,7 @@
}
for rows.Next() {
var dbentry databaseEntry
- if err = rows.Scan(&dbentry.email, &dbentry.caveats, &dbentry.revocationCaveatID, &dbentry.timestamp, &dbentry.blessings); err != nil {
+ if err = rows.Scan(&dbentry.email, &dbentry.caveats, &dbentry.timestamp, &dbentry.blessings); err != nil {
vlog.Errorf("scan of row failed %v", err)
dbentry.decodeErr = fmt.Errorf("failed to read sql row, %s", err)
}
diff --git a/services/identity/auditor/sql_database_test.go b/services/identity/auditor/sql_database_test.go
new file mode 100644
index 0000000..bbd2fa6
--- /dev/null
+++ b/services/identity/auditor/sql_database_test.go
@@ -0,0 +1,53 @@
+package auditor
+
+import (
+ "github.com/DATA-DOG/go-sqlmock"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestSQLDatabaseQuery(t *testing.T) {
+ db, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to create new mock database stub: %v", err)
+ }
+ columns := []string{"Email", "Caveat", "Timestamp", "Blessings"}
+ sqlmock.ExpectExec("CREATE TABLE IF NOT EXISTS tableName (.+)").
+ WillReturnResult(sqlmock.NewResult(0, 1))
+ d, err := newSQLDatabase(db, "tableName")
+ if err != nil {
+ t.Fatalf("failed to create SQLDatabase: %v", err)
+ }
+
+ entry := databaseEntry{
+ email: "email",
+ caveats: []byte("caveats"),
+ timestamp: time.Now(),
+ blessings: []byte("blessings"),
+ }
+ sqlmock.ExpectExec("INSERT INTO tableName (.+) VALUES (.+)").
+ WithArgs(entry.email, entry.caveats, entry.timestamp, entry.blessings).
+ WillReturnResult(sqlmock.NewResult(0, 1)) // no insert id, 1 affected row
+ if err := d.Insert(entry); err != nil {
+ t.Errorf("failed to insert into SQLDatabase: %v", err)
+ }
+
+ // Test the querying.
+ sqlmock.ExpectQuery("SELECT Email, Caveats, Timestamp, Blessings FROM tableName").
+ WithArgs(entry.email).
+ WillReturnRows(sqlmock.NewRows(columns).AddRow(entry.email, entry.caveats, entry.timestamp, entry.blessings))
+ ch := d.Query(entry.email)
+ if res := <-ch; !reflect.DeepEqual(res, entry) {
+ t.Errorf("got %#v, expected %#v", res, entry)
+ }
+
+ var extra bool
+ for _ = range ch {
+ // Drain the channel to prevent the producer goroutines from being leaked.
+ extra = true
+ }
+ if extra {
+ t.Errorf("Got more entries that expected")
+ }
+}
diff --git a/services/identity/blesser/oauth.go b/services/identity/blesser/oauth.go
index 7b59171..c50ed3a 100644
--- a/services/identity/blesser/oauth.go
+++ b/services/identity/blesser/oauth.go
@@ -122,11 +122,7 @@
var caveat security.Caveat
var err error
if b.revocationManager != nil {
- revocationCaveat, err := b.revocationManager.NewCaveat(self.PublicKey(), b.dischargerLocation)
- if err != nil {
- return noblessings, "", err
- }
- caveat, err = security.NewCaveat(revocationCaveat)
+ caveat, err = b.revocationManager.NewCaveat(self.PublicKey(), b.dischargerLocation)
} else {
caveat, err = security.ExpiryCaveat(time.Now().Add(b.duration))
}
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 109f3fd..2ac127b 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -430,11 +430,7 @@
if h.args.RevocationManager == nil {
return nil, fmt.Errorf("server not configured to support revocation")
}
- tpc, err := h.args.RevocationManager.NewCaveat(h.args.R.Principal().PublicKey(), h.args.DischargerLocation)
- if err != nil {
- return nil, fmt.Errorf("failed to create revocation caveat: %v", err)
- }
- revocation, err := security.NewCaveat(tpc)
+ revocation, err := h.args.RevocationManager.NewCaveat(h.args.R.Principal().PublicKey(), h.args.DischargerLocation)
if err != nil {
return nil, fmt.Errorf("failed to create revocation caveat: %v", err)
}
diff --git a/services/identity/googleoauth/template.go b/services/identity/googleoauth/template.go
index 2bd72bd..958c70e 100644
--- a/services/identity/googleoauth/template.go
+++ b/services/identity/googleoauth/template.go
@@ -171,18 +171,17 @@
<label class="col-sm-2" for="required-caveat">Expiration</label>
<div class="col-sm-10" class="input-group" name="required-caveat">
<div class="radio">
- <div class="input-group">
- <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry" checked>
- <input type="text" name="expiry" id="expiry" value="1h" placeholder="time after which the blessing will expire">
- </div>
- </div>
- <div class="radio">
<label>
- <!-- TODO(suharshs): Re-enable -->
- <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Revocation" disabled>
+ <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Revocation" checked>
When explicitly revoked
</label>
</div>
+ <div class="radio">
+ <div class="input-group">
+ <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry">
+ <input type="text" name="expiry" id="expiry" value="1h" placeholder="time after which the blessing will expire">
+ </div>
+ </div>
</div>
</div>
<h4 class="form-signin-heading">Additional caveats</h4>
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index 3372b0b..0219929 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -3,7 +3,7 @@
import (
"crypto/rand"
- "encoding/json"
+ "database/sql"
"flag"
"fmt"
"html/template"
@@ -42,18 +42,14 @@
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/'.")
- // 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.")
+ // Flag controlling auditing and revocation of Blessing operations.
+ sqlConfig = flag.String("sqlconfig", "", "Path to file containing go-sql-driver connection string of the following form: [username[:password]@][protocol[(address)]]/dbname")
// 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.")
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.")
googleConfigAndroid = flag.String("google_config_android", "", "Path to the JSON-encoded OAuth client configuration for Android 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")
-
- // Revocation/expiry configuration.
- // TODO(ashankar,ataly,suharshs): Re-enable by default once the move to the new security API is complete?
- revocationDir = flag.String("revocation_dir", "" /*filepath.Join(os.TempDir(), "revocation_dir")*/, "Path where the revocation manager will store caveat and revocation information.")
)
const (
@@ -64,14 +60,32 @@
func main() {
flag.Usage = usage
- p, blessingLogReader := providerPrincipal()
+ flag.Parse()
+
+ var sqlDB *sql.DB
+ var err error
+ if len(*sqlConfig) > 0 {
+ config, err := ioutil.ReadFile(*sqlConfig)
+ if err != nil {
+ vlog.Fatalf("failed to read sql config from %v", *sqlConfig)
+ }
+ sqlDB, err = dbFromConfigDatabase(strings.Trim(string(config), "\n"))
+ if err != nil {
+ vlog.Fatalf("failed to create sqlDB: %v", err)
+ }
+ }
+
+ p, blessingLogReader := providerPrincipal(sqlDB)
r := rt.Init(options.RuntimePrincipal{p})
defer r.Cleanup()
- // Calling with empty string returns a empty RevocationManager
- revocationManager, err := revocation.NewRevocationManager(*revocationDir)
- if err != nil {
- vlog.Fatalf("Failed to start RevocationManager: %v", err)
+ var revocationManager *revocation.RevocationManager
+ // Only set revocationManager sqlConfig (and thus sqlDB) is set.
+ if sqlDB != nil {
+ revocationManager, err = revocation.NewRevocationManager(sqlDB)
+ if err != nil {
+ vlog.Fatalf("Failed to start RevocationManager: %v", err)
+ }
}
// Setup handlers
@@ -119,7 +133,7 @@
if len(*googleConfigChrome) > 0 || len(*googleConfigAndroid) > 0 {
args.GoogleServers = appendSuffixTo(published, googleService)
}
- if len(*auditConfig) > 0 && len(*googleConfigWeb) > 0 {
+ if sqlDB != nil && len(*googleConfigWeb) > 0 {
args.ListBlessingsRoute = googleoauth.ListBlessingsRoute
}
if err := tmpl.Execute(w, args); err != nil {
@@ -281,7 +295,7 @@
// 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) {
+func providerPrincipal(sqlDB *sql.DB) (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,33 +303,25 @@
}
defer r.Cleanup()
p := r.Principal()
- if len(*auditConfig) == 0 {
+ if sqlDB == nil {
return p, nil
}
- config, err := readSQLConfigFromFile(*auditConfig)
- if err != nil {
- vlog.Fatalf("Failed to read sql config: %v", err)
- }
- auditor, reader, err := auditor.NewSQLBlessingAuditor(config)
+ auditor, reader, err := auditor.NewSQLBlessingAuditor(sqlDB)
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)
+func dbFromConfigDatabase(database string) (*sql.DB, error) {
+ db, err := sql.Open("mysql", database+"?parseTime=true")
if err != nil {
- return config, err
+ return nil, fmt.Errorf("failed to create database with database(%v): %v", database, err)
}
- if err := json.Unmarshal(content, &config); err != nil {
- return config, err
+ if err := db.Ping(); err != nil {
+ return nil, 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
+ return db, nil
}
func httpaddress() string {
diff --git a/services/identity/revocation/revocation_manager.go b/services/identity/revocation/revocation_manager.go
index 698e759..b4b2178 100644
--- a/services/identity/revocation/revocation_manager.go
+++ b/services/identity/revocation/revocation_manager.go
@@ -3,112 +3,89 @@
import (
"crypto/rand"
- "encoding/hex"
+ "database/sql"
"fmt"
- "path/filepath"
- "strconv"
"sync"
"time"
- "veyron.io/veyron/veyron/services/identity/util"
"veyron.io/veyron/veyron2/security"
"veyron.io/veyron/veyron2/vom"
)
// RevocationManager persists information for revocation caveats to provided discharges and allow for future revocations.
-type RevocationManager struct {
- caveatMap *util.DirectoryStore // Map of blessed identity's caveats. ThirdPartyCaveatID -> revocationCaveatID
+type RevocationManager struct{}
+
+// NewRevocationManager returns a RevocationManager that persists information about
+// revocationCaveats in a SQL database and allows for revocation and caveat creation.
+// This function can only be called once because of the use of global variables.
+func NewRevocationManager(sqlDB *sql.DB) (*RevocationManager, error) {
+ revocationLock.Lock()
+ defer revocationLock.Unlock()
+ if revocationDB != nil {
+ return nil, fmt.Errorf("NewRevocationManager can only be called once")
+ }
+ var err error
+ revocationDB, err = newSQLDatabase(sqlDB, "RevocationCaveatInfo")
+ if err != nil {
+ return nil, err
+ }
+ return &RevocationManager{}, nil
}
-var revocationMap *util.DirectoryStore
+var revocationDB database
var revocationLock sync.RWMutex
-// NewCaveat returns a security.ThirdPartyCaveat for which discharges will be
+// NewCaveat returns a security.Caveat constructed with a ThirdPartyCaveat for which discharges will be
// issued iff Revoke has not been called for the returned caveat.
-func (r *RevocationManager) NewCaveat(discharger security.PublicKey, dischargerLocation string) (security.ThirdPartyCaveat, error) {
+func (r *RevocationManager) NewCaveat(discharger security.PublicKey, dischargerLocation string) (security.Caveat, error) {
+ var empty security.Caveat
var revocation [16]byte
if _, err := rand.Read(revocation[:]); err != nil {
- return nil, err
+ return empty, err
}
restriction, err := security.NewCaveat(revocationCaveat(revocation))
if err != nil {
- return nil, err
+ return empty, err
}
cav, err := security.NewPublicKeyCaveat(discharger, dischargerLocation, security.ThirdPartyRequirements{}, restriction)
if err != nil {
- return nil, err
+ return empty, err
}
- if err = r.caveatMap.Put(hex.EncodeToString([]byte(cav.ID())), hex.EncodeToString(revocation[:])); err != nil {
- return nil, err
+ if err = revocationDB.InsertCaveat(cav.ID(), revocation[:]); err != nil {
+ return empty, err
}
- return cav, nil
+ return security.NewCaveat(cav)
}
// Revoke disables discharges from being issued for the provided third-party caveat.
func (r *RevocationManager) Revoke(caveatID string) error {
- token, err := r.caveatMap.Get(hex.EncodeToString([]byte(caveatID)))
- if err != nil {
- return err
- }
- return revocationMap.Put(token, strconv.FormatInt(time.Now().Unix(), 10))
+ return revocationDB.Revoke(caveatID)
}
// GetRevocationTimestamp returns the timestamp at which a caveat was revoked.
// If the caveat wasn't revoked returns nil
func (r *RevocationManager) GetRevocationTime(caveatID string) *time.Time {
- token, err := r.caveatMap.Get(hex.EncodeToString([]byte(caveatID)))
+ timestamp, err := revocationDB.RevocationTime(caveatID)
if err != nil {
return nil
}
- timestamp, err := revocationMap.Get(token)
- if err != nil {
- return nil
- }
- unix_int, err := strconv.ParseInt(timestamp, 10, 64)
- if err != nil {
- return nil
- }
- revocationTime := time.Unix(unix_int, 0)
- return &revocationTime
+ return timestamp
}
type revocationCaveat [16]byte
func (cav revocationCaveat) Validate(security.Context) error {
revocationLock.RLock()
- if revocationMap == nil {
+ if revocationDB == nil {
revocationLock.RUnlock()
return fmt.Errorf("missing call to NewRevocationManager")
}
revocationLock.RUnlock()
- if revocationMap.Exists(hex.EncodeToString(cav[:])) {
+ revoked, err := revocationDB.IsRevoked(cav[:])
+ if revoked {
return fmt.Errorf("revoked")
}
- return nil
-}
-
-// NewRevocationManager returns a RevocationManager that persists information about
-// revocationCaveats and allows for revocation and caveat creation.
-// This function can only be called once because of the use of global variables.
-func NewRevocationManager(dir string) (*RevocationManager, error) {
- revocationLock.Lock()
- defer revocationLock.Unlock()
- if revocationMap != nil {
- return nil, fmt.Errorf("NewRevocationManager can only be called once")
- }
- // If empty string return nil revocationManager
- if len(dir) == 0 {
- return nil, nil
- }
- caveatMap, err := util.NewDirectoryStore(filepath.Join(dir, "caveat_dir"))
- if err != nil {
- return nil, err
- }
- revocationMap, err = util.NewDirectoryStore(filepath.Join(dir, "revocation_dir"))
- if err != nil {
- return nil, err
- }
- return &RevocationManager{caveatMap}, nil
+ return err
}
func init() {
diff --git a/services/identity/revocation/revocation_test.go b/services/identity/revocation/revocation_test.go
new file mode 100644
index 0000000..171d036
--- /dev/null
+++ b/services/identity/revocation/revocation_test.go
@@ -0,0 +1,104 @@
+package revocation
+
+import (
+ "bytes"
+ "testing"
+ "time"
+
+ "veyron.io/veyron/veyron2"
+ "veyron.io/veyron/veyron2/naming"
+ "veyron.io/veyron/veyron2/rt"
+ "veyron.io/veyron/veyron2/security"
+ "veyron.io/veyron/veyron2/vom"
+
+ "veyron.io/veyron/veyron/profiles"
+ services "veyron.io/veyron/veyron/services/security"
+ "veyron.io/veyron/veyron/services/security/discharger"
+)
+
+type mockDatabase struct {
+ tpCavIDToRevCavID map[string][]byte
+ revCavIDToTimestamp map[string]*time.Time
+}
+
+func (m *mockDatabase) InsertCaveat(thirdPartyCaveatID string, revocationCaveatID []byte) error {
+ m.tpCavIDToRevCavID[thirdPartyCaveatID] = revocationCaveatID
+ return nil
+}
+
+func (m *mockDatabase) Revoke(thirdPartyCaveatID string) error {
+ timestamp := time.Now()
+ m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])] = ×tamp
+ return nil
+}
+
+func (m *mockDatabase) IsRevoked(revocationCaveatID []byte) (bool, error) {
+ _, exists := m.revCavIDToTimestamp[string(revocationCaveatID)]
+ return exists, nil
+}
+
+func (m *mockDatabase) RevocationTime(thirdPartyCaveatID string) (*time.Time, error) {
+ return m.revCavIDToTimestamp[string(m.tpCavIDToRevCavID[thirdPartyCaveatID])], nil
+}
+
+func newRevocationManager(t *testing.T) *RevocationManager {
+ revocationDB = &mockDatabase{make(map[string][]byte), make(map[string]*time.Time)}
+ return &RevocationManager{}
+}
+
+func revokerSetup(t *testing.T) (dischargerKey security.PublicKey, dischargerEndpoint string, revoker *RevocationManager, closeFunc func(), runtime veyron2.Runtime) {
+ r := rt.Init()
+ revokerService := newRevocationManager(t)
+ dischargerServer, err := r.NewServer()
+ if err != nil {
+ t.Fatalf("rt.R().NewServer: %s", err)
+ }
+ dischargerEP, err := dischargerServer.Listen(profiles.LocalListenSpec)
+ if err != nil {
+ t.Fatalf("dischargerServer.Listen failed: %v", err)
+ }
+ dischargerServiceStub := services.DischargerServer(discharger.NewDischarger())
+ if err := dischargerServer.Serve("", dischargerServiceStub, nil); err != nil {
+ t.Fatalf("dischargerServer.Serve revoker: %s", err)
+ }
+ return r.Principal().PublicKey(),
+ naming.JoinAddressName(dischargerEP.String(), ""),
+ revokerService,
+ func() {
+ dischargerServer.Stop()
+ },
+ r
+}
+
+func TestDischargeRevokeDischargeRevokeDischarge(t *testing.T) {
+ dcKey, dc, revoker, closeFunc, r := revokerSetup(t)
+ defer closeFunc()
+
+ discharger := services.DischargerClient(dc)
+ caveat, err := revoker.NewCaveat(dcKey, dc)
+ if err != nil {
+ t.Fatalf("failed to create revocation caveat: %s", err)
+ }
+ var cav security.ThirdPartyCaveat
+ if err := vom.NewDecoder(bytes.NewBuffer(caveat.ValidatorVOM)).Decode(&cav); err != nil {
+ t.Fatalf("failed to create decode tp caveat: %s", err)
+ }
+
+ var impetus security.DischargeImpetus
+
+ if _, err = discharger.Discharge(r.NewContext(), cav, impetus); err != nil {
+ t.Fatalf("failed to get discharge: %s", err)
+ }
+ if err = revoker.Revoke(cav.ID()); err != nil {
+ t.Fatalf("failed to revoke: %s", err)
+ }
+ if discharge, err := discharger.Discharge(r.NewContext(), cav, impetus); err == nil || discharge != nil {
+ t.Fatalf("got a discharge for a revoked caveat: %s", err)
+ }
+ if err = revoker.Revoke(cav.ID()); err != nil {
+ t.Fatalf("failed to revoke again: %s", err)
+ }
+ if discharge, err := discharger.Discharge(r.NewContext(), cav, impetus); err == nil || discharge != nil {
+ t.Fatalf("got a discharge for a doubly revoked caveat: %s", err)
+ }
+}
diff --git a/services/identity/revocation/revoker_test.go b/services/identity/revocation/revoker_test.go
deleted file mode 100644
index 7f5338b..0000000
--- a/services/identity/revocation/revoker_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package revocation
-
-import (
- "os"
- "path/filepath"
- "testing"
-
- "veyron.io/veyron/veyron2"
- "veyron.io/veyron/veyron2/naming"
- "veyron.io/veyron/veyron2/rt"
- "veyron.io/veyron/veyron2/security"
-
- "veyron.io/veyron/veyron/profiles"
- services "veyron.io/veyron/veyron/services/security"
- "veyron.io/veyron/veyron/services/security/discharger"
-)
-
-func revokerSetup(t *testing.T) (dischargerKey security.PublicKey, dischargerEndpoint string, revoker *RevocationManager, closeFunc func(), runtime veyron2.Runtime) {
- var dir = filepath.Join(os.TempDir(), "revoker_test_dir")
- r := rt.Init()
- revokerService, err := NewRevocationManager(dir)
- if err != nil {
- t.Fatalf("NewRevocationManager failed: %v", err)
- }
-
- dischargerServer, err := r.NewServer()
- if err != nil {
- t.Fatalf("rt.R().NewServer: %s", err)
- }
- dischargerEP, err := dischargerServer.Listen(profiles.LocalListenSpec)
- if err != nil {
- t.Fatalf("dischargerServer.Listen failed: %v", err)
- }
- dischargerServiceStub := services.DischargerServer(discharger.NewDischarger())
- if err := dischargerServer.Serve("", dischargerServiceStub, nil); err != nil {
- t.Fatalf("dischargerServer.Serve revoker: %s", err)
- }
- return r.Principal().PublicKey(),
- naming.JoinAddressName(dischargerEP.String(), ""),
- revokerService,
- func() {
- defer os.RemoveAll(dir)
- dischargerServer.Stop()
- },
- r
-}
-
-func TestDischargeRevokeDischargeRevokeDischarge(t *testing.T) {
- dcKey, dc, revoker, closeFunc, r := revokerSetup(t)
- defer closeFunc()
-
- discharger := services.DischargerClient(dc)
- cav, err := revoker.NewCaveat(dcKey, dc)
- if err != nil {
- t.Fatalf("failed to create public key caveat: %s", err)
- }
-
- var impetus security.DischargeImpetus
-
- if _, err = discharger.Discharge(r.NewContext(), cav, impetus); err != nil {
- t.Fatalf("failed to get discharge: %s", err)
- }
- if err = revoker.Revoke(cav.ID()); err != nil {
- t.Fatalf("failed to revoke: %s", err)
- }
- if discharge, err := discharger.Discharge(r.NewContext(), cav, impetus); err == nil || discharge != nil {
- t.Fatalf("got a discharge for a revoked caveat: %s", err)
- }
- if err = revoker.Revoke(cav.ID()); err != nil {
- t.Fatalf("failed to revoke again: %s", err)
- }
- if discharge, err := discharger.Discharge(r.NewContext(), cav, impetus); err == nil || discharge != nil {
- t.Fatalf("got a discharge for a doubly revoked caveat: %s", err)
- }
-}
diff --git a/services/identity/revocation/sql_database.go b/services/identity/revocation/sql_database.go
new file mode 100644
index 0000000..cfe5633
--- /dev/null
+++ b/services/identity/revocation/sql_database.go
@@ -0,0 +1,80 @@
+package revocation
+
+import (
+ "database/sql"
+ "encoding/hex"
+ "fmt"
+ "time"
+)
+
+type database interface {
+ InsertCaveat(thirdPartyCaveatID string, revocationCaveatID []byte) error
+ Revoke(thirdPartyCaveatID string) error
+ IsRevoked(revocationCaveatID []byte) (bool, error)
+ RevocationTime(thirdPartyCaveatID string) (*time.Time, error)
+}
+
+// Table with 3 columns:
+// (1) ThirdPartyCaveatID= string thirdPartyCaveatID.
+// (2) RevocationCaveatID= hex encoded revcationCaveatID.
+// (3) RevocationTime= time (if any) that the Caveat was revoked.
+type sqlDatabase struct {
+ insertCaveatStmt, revokeStmt, isRevokedStmt, revocationTimeStmt *sql.Stmt
+}
+
+func (s *sqlDatabase) InsertCaveat(thirdPartyCaveatID string, revocationCaveatID []byte) error {
+ _, err := s.insertCaveatStmt.Exec(thirdPartyCaveatID, hex.EncodeToString(revocationCaveatID))
+ return err
+}
+
+func (s *sqlDatabase) Revoke(thirdPartyCaveatID string) error {
+ _, err := s.revokeStmt.Exec(time.Now(), thirdPartyCaveatID)
+ return err
+}
+
+func (s *sqlDatabase) IsRevoked(revocationCaveatID []byte) (bool, error) {
+ rows, err := s.isRevokedStmt.Query(hex.EncodeToString(revocationCaveatID))
+ if err != nil {
+ return false, err
+ }
+ return rows.Next(), nil
+}
+
+func (s *sqlDatabase) RevocationTime(thirdPartyCaveatID string) (*time.Time, error) {
+ rows, err := s.revocationTimeStmt.Query(thirdPartyCaveatID)
+ if err != nil {
+ return nil, err
+ }
+ if rows.Next() {
+ var timestamp time.Time
+ if err := rows.Scan(×tamp); err != nil {
+ return nil, err
+ }
+ return ×tamp, nil
+ }
+ return nil, fmt.Errorf("the caveat (%v) was not revoked", thirdPartyCaveatID)
+}
+
+func newSQLDatabase(db *sql.DB, table string) (database, error) {
+ createStmt, err := db.Prepare(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s ( ThirdPartyCaveatID NVARCHAR(255), RevocationCaveatID NVARCHAR(255), RevocationTime DATETIME, PRIMARY KEY (ThirdPartyCaveatID), KEY (RevocationCaveatID) );", table))
+ if err != nil {
+ return nil, err
+ }
+ if _, err = createStmt.Exec(); err != nil {
+ return nil, err
+ }
+ insertCaveatStmt, err := db.Prepare(fmt.Sprintf("INSERT INTO %s (ThirdPartyCaveatID, RevocationCaveatID, RevocationTime) VALUES (?, ?, NULL)", table))
+ if err != nil {
+ return nil, err
+ }
+ revokeStmt, err := db.Prepare(fmt.Sprintf("UPDATE %s SET RevocationTime=? WHERE ThirdPartyCaveatID=?", table))
+ if err != nil {
+ return nil, err
+ }
+ isRevokedStmt, err := db.Prepare(fmt.Sprintf("SELECT 1 FROM %s WHERE RevocationCaveatID=? AND RevocationTime IS NOT NULL", table))
+ if err != nil {
+ return nil, err
+ }
+ revocationTimeStmt, err := db.Prepare(fmt.Sprintf("SELECT RevocationTime FROM %s WHERE ThirdPartyCaveatID=?", table))
+ return &sqlDatabase{insertCaveatStmt, revokeStmt, isRevokedStmt, revocationTimeStmt}, err
+}
diff --git a/services/identity/revocation/sql_database_test.go b/services/identity/revocation/sql_database_test.go
new file mode 100644
index 0000000..de9171b
--- /dev/null
+++ b/services/identity/revocation/sql_database_test.go
@@ -0,0 +1,73 @@
+package revocation
+
+import (
+ "encoding/hex"
+ "github.com/DATA-DOG/go-sqlmock"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestSQLDatabase(t *testing.T) {
+ db, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to create new mock database stub: %v", err)
+ }
+ columns := []string{"ThirdPartyCaveatID", "RevocationCaveatID", "RevocationTime"}
+ sqlmock.ExpectExec("CREATE TABLE IF NOT EXISTS tableName (.+)").
+ WillReturnResult(sqlmock.NewResult(0, 1))
+ d, err := newSQLDatabase(db, "tableName")
+ if err != nil {
+ t.Fatalf("failed to create SQLDatabase: %v", err)
+ }
+
+ tpCavID, revCavID := "tpCavID", []byte("revCavID")
+ tpCavID2, revCavID2 := "tpCavID2", []byte("revCavID2")
+ encRevCavID := hex.EncodeToString(revCavID)
+ encRevCavID2 := hex.EncodeToString(revCavID2)
+ sqlmock.ExpectExec("INSERT INTO tableName (.+) VALUES (.+)").
+ WithArgs(tpCavID, encRevCavID).
+ WillReturnResult(sqlmock.NewResult(0, 1)) // no insert id, 1 affected row
+ if err := d.InsertCaveat(tpCavID, revCavID); err != nil {
+ t.Errorf("failed to InsertCaveat into SQLDatabase: %v", err)
+ }
+
+ sqlmock.ExpectExec("INSERT INTO tableName (.+) VALUES (.+)").
+ WithArgs(tpCavID2, encRevCavID2).
+ WillReturnResult(sqlmock.NewResult(0, 1)) // no insert id, 1 affected row
+ if err := d.InsertCaveat(tpCavID2, revCavID2); err != nil {
+ t.Errorf("second InsertCaveat into SQLDatabase failed: %v", err)
+ }
+
+ // Test Revocation
+ sqlmock.ExpectExec("UPDATE tableName SET RevocationTime=.+").
+ WillReturnResult(sqlmock.NewResult(0, 1))
+ if err := d.Revoke(tpCavID); err != nil {
+ t.Errorf("failed to Revoke Caveat: %v", err)
+ }
+
+ // Test IsRevoked returns true.
+ sqlmock.ExpectQuery("SELECT 1 FROM tableName").
+ WithArgs(encRevCavID).
+ WillReturnRows(sqlmock.NewRows(columns).AddRow(1, 1, 1))
+ if revoked, err := d.IsRevoked(revCavID); err != nil || !revoked {
+ t.Errorf("expected revCavID to be revoked: err: (%v)", err)
+ }
+
+ // Test IsRevoked returns false.
+ sqlmock.ExpectQuery("SELECT 1 FROM tableName").
+ WithArgs(encRevCavID2).
+ WillReturnRows(sqlmock.NewRows(columns))
+ if revoked, err := d.IsRevoked(revCavID2); err != nil || revoked {
+ t.Errorf("expected revCavID to not be revoked: err: (%v)", err)
+ }
+
+ // Test RevocationTime.
+ revocationTime := time.Now()
+ sqlmock.ExpectQuery("SELECT RevocationTime FROM tableName").
+ WithArgs(tpCavID).
+ WillReturnRows(sqlmock.NewRows([]string{"RevocationTime"}).AddRow(revocationTime))
+ if got, err := d.RevocationTime(tpCavID); err != nil || !reflect.DeepEqual(*got, revocationTime) {
+ t.Errorf("got %v, expected %v: err : %v", got, revocationTime, err)
+ }
+}
diff --git a/services/identity/util/directory_store.go b/services/identity/util/directory_store.go
deleted file mode 100644
index 7c93638..0000000
--- a/services/identity/util/directory_store.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package util
-
-import (
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
-)
-
-// DirectoryStore implements a key-value store on a filesystem where data for each key is stored in its own file.
-// TODO(suharshs): When vstore is ready replace this with the veyron store.
-type DirectoryStore struct {
- dir string
-}
-
-func (s DirectoryStore) Exists(key string) bool {
- _, err := os.Stat(s.pathName(key))
- return !os.IsNotExist(err)
-}
-
-func (s DirectoryStore) Put(key, value string) error {
- return ioutil.WriteFile(s.pathName(key), []byte(value), 0600)
-}
-
-func (s DirectoryStore) Get(key string) (string, error) {
- bytes, err := ioutil.ReadFile(s.pathName(key))
- return string(bytes), err
-}
-
-func (s DirectoryStore) pathName(key string) string {
- return filepath.Join(string(s.dir), key)
-}
-
-// NewDirectoryStore returns a key-value store that uses one file per key,
-// and places all data in the provided directory.
-func NewDirectoryStore(dir string) (*DirectoryStore, error) {
- if len(dir) == 0 {
- return nil, fmt.Errorf("must provide non-empty directory name")
- }
- // Make the directory if it doesn't already exist.
- if err := os.MkdirAll(dir, 0700); err != nil {
- return nil, err
- }
-
- return &DirectoryStore{dir}, nil
-}
diff --git a/services/identity/util/sql_config.go b/services/identity/util/sql_config.go
new file mode 100644
index 0000000..f375d63
--- /dev/null
+++ b/services/identity/util/sql_config.go
@@ -0,0 +1,18 @@
+package util
+
+import (
+ "database/sql"
+ "fmt"
+ _ "github.com/go-sql-driver/mysql"
+)
+
+func DBFromConfigDatabase(database string) (*sql.DB, error) {
+ db, err := sql.Open("mysql", database+"?parseTime=true")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create database with database(%v): %v", database, err)
+ }
+ if err := db.Ping(); err != nil {
+ return nil, err
+ }
+ return db, nil
+}