From 6b6b294e30ece1beb7e12edc2a5569369dc0f18a Mon Sep 17 00:00:00 2001 From: Saad Karim Date: Thu, 16 Nov 2017 10:15:00 -0500 Subject: [PATCH] [FAB-6647] 1. Maintain backwards compatibility Need to maintain backwards compatibility for any functionality that is added that affect functionality of current users. Version checks are made on the server configuration file, server executable, and database version. Based on the versions it is determined if a migration is needed of existing identities to work with the latest release of fabric-ca-server. This changeset lays down the framework that will check if migration is needed. Next changeset will add the migration logic. Change-Id: Ie8c2b31aef598d69025664335da96d4a13330cb0 Signed-off-by: Saad Karim --- Makefile | 2 +- cmd/fabric-ca-client/clientcmd.go | 2 +- cmd/fabric-ca-client/main_test.go | 14 ++- cmd/fabric-ca-server/config.go | 7 +- cmd/fabric-ca-server/main_test.go | 6 + cmd/fabric-ca-server/servercmd.go | 2 +- cmd/metadata.go | 37 ------ cmd/metadata_test.go | 33 ----- docs/source/serverconfig.rst | 9 +- lib/ca.go | 132 ++++++++++++++++---- lib/caconfig.go | 1 + lib/certdbaccessor.go | 12 +- lib/client_whitebox_test.go | 1 + lib/dasqlite_test.go | 102 +++++++++++++--- lib/dbaccessor.go | 185 ++++++++++++++++++++++++++--- lib/dbutil/dbutil.go | 116 +++++++++++++++--- lib/ldap/client.go | 17 ++- lib/metadata/version.go | 142 ++++++++++++++++++++++ lib/metadata/version_test.go | 84 +++++++++++++ lib/server.go | 15 +-- lib/server_test.go | 6 + lib/server_whitebox_test.go | 4 +- lib/servererror.go | 4 + lib/spi/affiliation.go | 24 +++- lib/spi/affiliation_test.go | 13 +- lib/spi/userregistry.go | 9 +- scripts/fvt/backwards_comp_test.sh | 133 +++++++++++++++++++++ scripts/fvt/db_test.sh | 7 -- scripts/fvt/fabric-ca_setup.sh | 2 +- scripts/fvt/fabric-ca_utils | 6 + scripts/regenDocs | 2 +- 31 files changed, 944 insertions(+), 185 deletions(-) delete mode 100644 cmd/metadata.go delete mode 100644 cmd/metadata_test.go create mode 100644 lib/metadata/version.go create mode 100644 lib/metadata/version_test.go create mode 100755 scripts/fvt/backwards_comp_test.sh diff --git a/Makefile b/Makefile index 5dfe8cdfd..4ef34a4ad 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ PKGNAME = github.com/hyperledger/$(PROJECT_NAME) METADATA_VAR = Version=$(PROJECT_VERSION) GO_SOURCE := $(shell find . -name '*.go') -GO_LDFLAGS = $(patsubst %,-X $(PKGNAME)/cmd.%,$(METADATA_VAR)) +GO_LDFLAGS = $(patsubst %,-X $(PKGNAME)/lib/metadata.%,$(METADATA_VAR)) export GO_LDFLAGS DOCKER_ORG = hyperledger diff --git a/cmd/fabric-ca-client/clientcmd.go b/cmd/fabric-ca-client/clientcmd.go index 8aa29c604..7bbfab86a 100644 --- a/cmd/fabric-ca-client/clientcmd.go +++ b/cmd/fabric-ca-client/clientcmd.go @@ -24,8 +24,8 @@ import ( "strings" "github.com/cloudflare/cfssl/log" - "github.com/hyperledger/fabric-ca/cmd" "github.com/hyperledger/fabric-ca/lib" + "github.com/hyperledger/fabric-ca/lib/metadata" "github.com/hyperledger/fabric-ca/util" "github.com/pkg/profile" "github.com/spf13/cobra" diff --git a/cmd/fabric-ca-client/main_test.go b/cmd/fabric-ca-client/main_test.go index b95ce925f..da575f683 100644 --- a/cmd/fabric-ca-client/main_test.go +++ b/cmd/fabric-ca-client/main_test.go @@ -37,6 +37,7 @@ import ( "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib" "github.com/hyperledger/fabric-ca/lib/dbutil" + "github.com/hyperledger/fabric-ca/lib/metadata" "github.com/hyperledger/fabric-ca/util" "github.com/hyperledger/fabric/common/attrmgr" "github.com/stretchr/testify/assert" @@ -130,6 +131,11 @@ type TestData struct { input []string // input } +func TestMain(m *testing.M) { + metadata.Version = "1.1.0" + os.Exit(m.Run()) +} + func TestNoArguments(t *testing.T) { err := RunMain([]string{cmdName}) if err == nil { @@ -816,8 +822,7 @@ func testRegisterCommandLine(t *testing.T, srv *lib.Server) { sqliteDB, err := dbutil.NewUserRegistrySQLLite3(srv.CA.Config.DB.Datasource) assert.NoError(t, err) - db := lib.NewDBAccessor() - db.SetDB(sqliteDB) + db := lib.NewDBAccessor(sqliteDB) user, err := db.GetUser("testRegister3", nil) assert.NoError(t, err) @@ -1043,8 +1048,7 @@ func testAffiliation(t *testing.T) { sqliteDB, err := dbutil.NewUserRegistrySQLLite3(srv.CA.Config.DB.Datasource) assert.NoError(t, err) - db := lib.NewDBAccessor() - db.SetDB(sqliteDB) + db := lib.NewDBAccessor(sqliteDB) user, err := db.GetUser("testRegister6", nil) assert.NoError(t, err) @@ -1501,7 +1505,7 @@ func getSerialAKIByID(id string) (serial, aki string, err error) { if err != nil { return "", "", err } - acc := lib.NewCertDBAccessor(testdb) + acc := lib.NewCertDBAccessor(testdb, 0) certs, err := acc.GetCertificatesByID(id) if err != nil { diff --git a/cmd/fabric-ca-server/config.go b/cmd/fabric-ca-server/config.go index 316523d35..06cf3a427 100644 --- a/cmd/fabric-ca-server/config.go +++ b/cmd/fabric-ca-server/config.go @@ -26,6 +26,7 @@ import ( "github.com/cloudflare/cfssl/log" "github.com/hyperledger/fabric-ca/lib" + "github.com/hyperledger/fabric-ca/lib/metadata" "github.com/hyperledger/fabric-ca/util" ) @@ -77,6 +78,9 @@ const ( # ############################################################################# +# Version of config file +version: <<>> + # Server's listening port (default: 7054) port: 7054 @@ -481,7 +485,8 @@ func (s *ServerCmd) createDefaultConfigFile() error { } // Do string subtitution to get the default config - cfg := strings.Replace(defaultCfgTemplate, "<<>>", user, 1) + cfg := strings.Replace(defaultCfgTemplate, "<<>>", metadata.Version, 1) + cfg = strings.Replace(cfg, "<<>>", user, 1) cfg = strings.Replace(cfg, "<<>>", pass, 1) cfg = strings.Replace(cfg, "<<>>", myhost, 1) purl := s.myViper.GetString("intermediate.parentserver.url") diff --git a/cmd/fabric-ca-server/main_test.go b/cmd/fabric-ca-server/main_test.go index 4a94f27d3..440cbb6ac 100644 --- a/cmd/fabric-ca-server/main_test.go +++ b/cmd/fabric-ca-server/main_test.go @@ -29,6 +29,7 @@ import ( "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib" + "github.com/hyperledger/fabric-ca/lib/metadata" "github.com/hyperledger/fabric-ca/util" "github.com/stretchr/testify/assert" ) @@ -89,6 +90,11 @@ func errorTest(in *TestData, t *testing.T) { } } +func TestMain(m *testing.M) { + metadata.Version = "1.1.0" + os.Exit(m.Run()) +} + func TestNoArguments(t *testing.T) { err := RunMain([]string{cmdName}) if err == nil { diff --git a/cmd/fabric-ca-server/servercmd.go b/cmd/fabric-ca-server/servercmd.go index db8f302b9..7801fbc87 100644 --- a/cmd/fabric-ca-server/servercmd.go +++ b/cmd/fabric-ca-server/servercmd.go @@ -22,8 +22,8 @@ import ( "strings" "github.com/cloudflare/cfssl/log" - "github.com/hyperledger/fabric-ca/cmd" "github.com/hyperledger/fabric-ca/lib" + "github.com/hyperledger/fabric-ca/lib/metadata" "github.com/hyperledger/fabric-ca/util" "github.com/pkg/errors" "github.com/spf13/cobra" diff --git a/cmd/metadata.go b/cmd/metadata.go deleted file mode 100644 index ad2f908c0..000000000 --- a/cmd/metadata.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright IBM Corp. 2017 All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package metadata - -import ( - "fmt" - "runtime" -) - -// Version specifies fabric-ca-client/fabric-ca-server version -// It is defined by the Makefile and passed in with ldflags -var Version string - -// GetVersionInfo returns version information for the fabric-ca-client/fabric-ca-server -func GetVersionInfo(prgName string) string { - if Version == "" { - Version = "development build" - } - - return fmt.Sprintf("%s:\n Version: %s\n Go version: %s\n OS/Arch: %s\n", - prgName, Version, runtime.Version(), - fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)) -} diff --git a/cmd/metadata_test.go b/cmd/metadata_test.go deleted file mode 100644 index 2b5cb4c00..000000000 --- a/cmd/metadata_test.go +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright IBM Corp. 2017 All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package metadata_test - -import ( - "testing" - - "github.com/hyperledger/fabric-ca/cmd" - "github.com/stretchr/testify/assert" -) - -func TestGetVersionInfo(t *testing.T) { - info := metadata.GetVersionInfo("fabric-ca-client") - assert.Contains(t, info, "Version: development build") - - metadata.Version = "1.0.0" - info = metadata.GetVersionInfo("fabric-ca-client") - assert.Contains(t, info, "Version: 1.0.0") -} diff --git a/docs/source/serverconfig.rst b/docs/source/serverconfig.rst index 53560d36a..9f4d150a8 100644 --- a/docs/source/serverconfig.rst +++ b/docs/source/serverconfig.rst @@ -41,6 +41,9 @@ Fabric-CA Server's Configuration File # ############################################################################# + # Version of config file + version: <<>> + # Server's listening port (default: 7054) port: 7054 @@ -129,10 +132,9 @@ Fabric-CA Server's Configuration File pass: <<>> type: client affiliation: "" - maxenrollments: -1 attrs: - hf.Registrar.Roles: "client,user,peer,validator,auditor" - hf.Registrar.DelegateRoles: "client,user,validator,auditor" + hf.Registrar.Roles: "peer,orderer,client,user" + hf.Registrar.DelegateRoles: "peer,orderer,client,user" hf.Revoker: true hf.IntermediateCA: true hf.GenCRL: true @@ -214,6 +216,7 @@ Fabric-CA Server's Configuration File ca: usage: - cert sign + - crl sign expiry: 43800h caconstraint: isca: true diff --git a/lib/ca.go b/lib/ca.go index ca0d8e5ab..c3e021192 100644 --- a/lib/ca.go +++ b/lib/ca.go @@ -41,6 +41,7 @@ import ( "github.com/hyperledger/fabric-ca/api" "github.com/hyperledger/fabric-ca/lib/dbutil" "github.com/hyperledger/fabric-ca/lib/ldap" + "github.com/hyperledger/fabric-ca/lib/metadata" "github.com/hyperledger/fabric-ca/lib/spi" "github.com/hyperledger/fabric-ca/lib/tcert" "github.com/hyperledger/fabric-ca/lib/tls" @@ -99,8 +100,8 @@ type CA struct { server *Server // Indicates if database was successfully initialized dbInitialized bool - // Indicates if errors encountered during db initialization - dbError bool + // DB levels + levels *dbutil.Levels // CA mutex mutex sync.Mutex } @@ -408,6 +409,9 @@ func (ca *CA) initConfig() (err error) { } // Set config defaults cfg := ca.Config + if cfg.Version == "" { + cfg.Version = "0" + } if cfg.CA.Certfile == "" { cfg.CA.Certfile = "ca-cert.pem" } @@ -441,7 +445,10 @@ func (ca *CA) initConfig() (err error) { defaultIssuedCertificateExpiration, false) cs.Profiles["tls"] = tlsProfile - + err = ca.checkConfigLevels() + if err != nil { + return err + } // Set log level if debug is true if ca.server != nil && ca.server.Config != nil && ca.server.Config.Debug { log.Level = log.LevelDebug @@ -502,15 +509,6 @@ func (ca *CA) getVerifyOptions() (*x509.VerifyOptions, error) { func (ca *CA) initDB() error { log.Debug("Initializing DB") - initFunc := func(fn func() error) error { - initErr := fn() - if initErr != nil { - ca.dbError = true - log.Error(initErr) - } - return initErr - } - // If DB is initialized, don't need to proceed further if ca.dbInitialized { return nil @@ -525,7 +523,7 @@ func (ca *CA) initDB() error { } db := &ca.Config.DB - ca.dbError = false + dbError := false var err error if db.Type == "" || db.Type == defaultDatabaseType { @@ -575,7 +573,7 @@ func (ca *CA) initDB() error { } // Set the certificate DB accessor - ca.certDBAccessor = NewCertDBAccessor(ca.db) + ca.certDBAccessor = NewCertDBAccessor(ca.db, ca.levels.Certificate) // If DB initialization fails and we need to reinitialize DB, need to make sure to set the DB accessor for the signer if ca.enrollSigner != nil { @@ -583,19 +581,41 @@ func (ca *CA) initDB() error { } // Initialize user registry to either use DB or LDAP - err = initFunc(ca.initUserRegistry) + err = ca.initUserRegistry() if err != nil { return err } - // If not using LDAP, load the affiliations table + + // If not using LDAP, migrate database if needed to latest version and load the users and affiliations table if !ca.Config.LDAP.Enabled { - err = initFunc(ca.loadAffiliationsTable) + err = ca.checkDBLevels() if err != nil { return err } + + err = ca.loadUsersTable() + if err != nil { + log.Error(err) + dbError = true + if isFatalError(err) { + return err + } + } + + err = ca.loadAffiliationsTable() + if err != nil { + log.Error(err) + dbError = true + } + + err = ca.performMigration() + if err != nil { + log.Error(err) + dbError = true + } } - if ca.dbError { + if dbError { return errors.Errorf("Failed to initialize %s database at %s ", db.Type, ds) } @@ -639,10 +659,6 @@ func (ca *CA) initUserRegistry() error { dbAccessor := new(Accessor) dbAccessor.SetDB(ca.db) ca.registry = dbAccessor - err = ca.loadUsersTable() - if err != nil { - return err - } log.Debug("Initialized DB identity registry") return nil } @@ -776,6 +792,7 @@ func (ca *CA) addIdentity(id *CAConfigIdentity, errIfFound bool) error { Affiliation: id.Affiliation, Attributes: ca.convertAttrs(id.Attrs), MaxEnrollments: id.MaxEnrollments, + Level: ca.levels.Identity, } err = ca.registry.InsertUser(rec) if err != nil { @@ -786,7 +803,7 @@ func (ca *CA) addIdentity(id *CAConfigIdentity, errIfFound bool) error { } func (ca *CA) addAffiliation(path, parentPath string) error { - return ca.registry.InsertAffiliation(path, parentPath) + return ca.registry.InsertAffiliation(path, parentPath, ca.levels.Affiliation) } // CertDBAccessor returns the certificate DB accessor for CA @@ -1077,6 +1094,63 @@ func (ca *CA) loadCNFromEnrollmentInfo(certFile string) (string, error) { return name, nil } +func (ca *CA) performMigration() error { + log.Debug("Checking and performing migration, if needed") + + // TODO: Migration logic will go here + + sl, err := metadata.GetLevels(metadata.GetVersion()) + if err != nil { + return err + } + err = dbutil.UpdateDBLevel(ca.db, sl) + if err != nil { + return errors.Wrap(err, "Failed to correctly update level of tables in the database") + } + + return nil +} + +func (ca *CA) checkConfigLevels() error { + serverVersion := metadata.GetVersion() + configVersion := ca.Config.Version + log.Debugf("Checking configuration file verion '%+v' against server version: '%+v'", configVersion, serverVersion) + // Check configuration file version against server version to make sure that newer configuration file is not being used with server + cmp, err := metadata.CmpVersion(configVersion, serverVersion) + if err != nil { + return errors.WithMessage(err, "Failed to compare version") + } + if cmp == -1 { + return fmt.Errorf("Configuration file version '%s' is higher than server version '%s'", configVersion, serverVersion) + } + cfg, err := metadata.GetLevels(ca.Config.Version) + if err != nil { + return err + } + ca.levels = cfg + return nil +} + +func (ca *CA) checkDBLevels() error { + // Check database table levels against server levels to make sure that a database levels are compatible with server + levels, err := ca.registry.GetProperties([]string{"identity.level", "affiliation.level", "certificate.level"}) + if err != nil { + return err + } + sl, err := metadata.GetLevels(metadata.GetVersion()) + if err != nil { + return err + } + log.Debugf("Checking database levels '%+v' against server levels '%+v'", levels, sl) + idVer := getIntLevel(levels, "identity") + affVer := getIntLevel(levels, "affiliation") + certVer := getIntLevel(levels, "certificate") + if (idVer > sl.Identity) || (affVer > sl.Affiliation) || (certVer > sl.Certificate) { + return newFatalError(ErrDBLevel, "The version of the database is newer than the server version. Upgrade your server.") + } + return nil +} + func writeFile(file string, buf []byte, perm os.FileMode) error { err := os.MkdirAll(filepath.Dir(file), 0755) if err != nil { @@ -1118,3 +1192,15 @@ func initSigningProfile(spp **config.SigningProfile, expiry time.Duration, isCA // This is set so that all profiles permit an attribute extension in CFSSL sp.ExtensionWhitelist[attrmgr.AttrOIDString] = true } + +func getIntLevel(properties map[string]string, version string) int { + strVersion := properties[version] + if strVersion == "" { + strVersion = "0" + } + intVersion, err := strconv.Atoi(strVersion) + if err != nil { + panic(err) + } + return intVersion +} diff --git a/lib/caconfig.go b/lib/caconfig.go index e26d38735..78e4ae569 100644 --- a/lib/caconfig.go +++ b/lib/caconfig.go @@ -84,6 +84,7 @@ csr: // "help" - the help message to display on the command line; // "skip" - to skip the field. type CAConfig struct { + Version string `skip:"true"` CA CAInfo Signing *config.Signing CSR api.CSRInfo diff --git a/lib/certdbaccessor.go b/lib/certdbaccessor.go index b1a98ccf5..b3aa1d34d 100644 --- a/lib/certdbaccessor.go +++ b/lib/certdbaccessor.go @@ -35,8 +35,8 @@ import ( const ( insertSQL = ` -INSERT INTO certificates (id, serial_number, authority_key_identifier, ca_label, status, reason, expiry, revoked_at, pem) - VALUES (:id, :serial_number, :authority_key_identifier, :ca_label, :status, :reason, :expiry, :revoked_at, :pem);` +INSERT INTO certificates (id, serial_number, authority_key_identifier, ca_label, status, reason, expiry, revoked_at, pem, level) + VALUES (:id, :serial_number, :authority_key_identifier, :ca_label, :status, :reason, :expiry, :revoked_at, :pem, :level);` selectSQLbyID = ` SELECT %s FROM certificates @@ -62,21 +62,24 @@ SELECT %s FROM certificates // CertRecord extends CFSSL CertificateRecord by adding an enrollment ID to the record type CertRecord struct { - ID string `db:"id"` + ID string `db:"id"` + Level int `db:"level"` certdb.CertificateRecord } // CertDBAccessor implements certdb.Accessor interface. type CertDBAccessor struct { + level int accessor certdb.Accessor db *sqlx.DB } // NewCertDBAccessor returns a new Accessor. -func NewCertDBAccessor(db *sqlx.DB) *CertDBAccessor { +func NewCertDBAccessor(db *sqlx.DB, level int) *CertDBAccessor { cffslAcc := new(CertDBAccessor) cffslAcc.db = db cffslAcc.accessor = certsql.NewAccessor(db) + cffslAcc.level = level return cffslAcc } @@ -124,6 +127,7 @@ func (d *CertDBAccessor) InsertCertificate(cr certdb.CertificateRecord) error { record.Expiry = cr.Expiry.UTC() record.RevokedAt = cr.RevokedAt.UTC() record.PEM = cr.PEM + record.Level = d.level res, err := d.db.NamedExec(insertSQL, record) if err != nil { diff --git a/lib/client_whitebox_test.go b/lib/client_whitebox_test.go index 9647fb065..ad25ad770 100644 --- a/lib/client_whitebox_test.go +++ b/lib/client_whitebox_test.go @@ -508,6 +508,7 @@ func TestCWBCAConfig(t *testing.T) { t.Errorf("initConfig failed: %s", err) } ca = &CA{} + ca.server = &Server{} err = ca.initConfig() if err != nil { t.Errorf("ca.initConfig default failed: %s", err) diff --git a/lib/dasqlite_test.go b/lib/dasqlite_test.go index 00b4d1488..156fa488f 100644 --- a/lib/dasqlite_test.go +++ b/lib/dasqlite_test.go @@ -65,8 +65,7 @@ func TestSQLite(t *testing.T) { if err != nil { t.Error("Failed to open connection to DB") } - accessor := NewDBAccessor() - accessor.SetDB(db) + accessor := NewDBAccessor(db) ta := TestAccessor{ Accessor: accessor, @@ -119,8 +118,7 @@ func createSQLiteDB(path string, t *testing.T) (*sqlx.DB, *TestAccessor) { db, err := sqlx.Open("sqlite3", path) assert.NoError(t, err, "Failed to open SQLite database") - accessor := NewDBAccessor() - accessor.SetDB(db) + accessor := NewDBAccessor(db) ta := &TestAccessor{ Accessor: accessor, @@ -138,7 +136,7 @@ func testWithExistingDbAndTablesAndUser(t *testing.T) { os.Remove(rootDB) db, acc := createSQLiteDB(rootDB, t) - _, err = db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(64), token bytea, type VARCHAR(64), affiliation VARCHAR(64), attributes VARCHAR(256), state INTEGER, max_enrollments INTEGER)") + _, err = db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(64), token bytea, type VARCHAR(64), affiliation VARCHAR(64), attributes VARCHAR(256), state INTEGER, max_enrollments INTEGER, level INTEGER DEFAULT 0)") assert.NoError(t, err, "Error creating users table") srv := TestGetServer2(false, rootPort, rootDir, "", -1, t) @@ -176,7 +174,7 @@ func testWithExistingDbAndTable(t *testing.T) { srv := TestGetServer2(false, rootPort, rootDir, "", -1, t) srv.CA.Config.DB.Datasource = "fabric_ca.db" - _, err = db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(64), token bytea, type VARCHAR(64), affiliation VARCHAR(64), attributes VARCHAR(256), state INTEGER, max_enrollments INTEGER)") + _, err = db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(64), token bytea, type VARCHAR(64), affiliation VARCHAR(64), attributes VARCHAR(256), state INTEGER, max_enrollments INTEGER, level INTEGER DEFAULT 0)") assert.NoError(t, err, "Error creating users table") err = srv.Start() @@ -221,10 +219,11 @@ func removeDatabase() { func testEverything(ta TestAccessor, t *testing.T) { testInsertAndGetUser(ta, t) - testDeleteUser(ta, t) - testUpdateUser(ta, t) - testInsertAndGetAffiliation(ta, t) - testDeleteAffiliation(ta, t) + testModifyAttribute(ta, t) + // testDeleteUser(ta, t) + // testUpdateUser(ta, t) + // testInsertAndGetAffiliation(ta, t) + // testDeleteAffiliation(ta, t) } func testInsertAndGetUser(ta TestAccessor, t *testing.T) { @@ -232,10 +231,27 @@ func testInsertAndGetUser(ta TestAccessor, t *testing.T) { ta.Truncate() insert := spi.UserInfo{ - Name: "testId", - Pass: "123456", - Type: "client", - Attributes: []api.Attribute{}, + Name: "testId", + Pass: "123456", + Type: "client", + Attributes: []api.Attribute{ + api.Attribute{ + Name: "hf.Registrar.Roles", + Value: "peer,client,orderer,user", + }, + api.Attribute{ + Name: "hf.Revoker", + Value: "false", + }, + api.Attribute{ + Name: "hf.Registrar.Attributes", + Value: "*", + }, + api.Attribute{ + Name: "xyz", + Value: "xyz", + }, + }, } err := ta.Accessor.InsertUser(insert) @@ -253,6 +269,55 @@ func testInsertAndGetUser(ta TestAccessor, t *testing.T) { } } +func testModifyAttribute(ta TestAccessor, t *testing.T) { + + user, err := ta.Accessor.GetUser("testId", nil) + assert.NoError(t, err, "Failed to get user") + + err = user.ModifyAttributes([]api.Attribute{ + api.Attribute{ + Name: "hf.Registrar.Roles", + Value: "peer", + }, + api.Attribute{ + Name: "hf.Revoker", + Value: "", + }, + api.Attribute{ + Name: "xyz", + Value: "", + }, + api.Attribute{ + Name: "hf.IntermediateCA", + Value: "true", + }, + }) + assert.NoError(t, err, "Failed to modify user's attributes") + + user, err = ta.Accessor.GetUser("testId", nil) + assert.NoError(t, err, "Failed to get user") + + _, err = user.GetAttribute("hf.Revoker") + assert.Error(t, err, "Should have returned an error, attribute should have been deleted") + + // Removes last attribute in the slice, should have correctly removed it + _, err = user.GetAttribute("xyz") + assert.Error(t, err, "Should have returned an error, attribute should have been deleted") + + attr, err := user.GetAttribute("hf.IntermediateCA") + assert.NoError(t, err, "Failed to add attribute") + assert.Equal(t, "true", attr.Value, "Incorrect value for attribute 'hf.IntermediateCA") + + attr, err = user.GetAttribute("hf.Registrar.Roles") + assert.NoError(t, err, "Failed to get attribute") + assert.Equal(t, "peer", attr.Value, "Incorrect value for attribute 'hf.Registrar.Roles") + + // Test to make sure that any existing attributes that were not modified continue to exist in there original state + attr, err = user.GetAttribute("hf.Registrar.Attributes") + assert.NoError(t, err, "Failed to get attribute") + assert.Equal(t, "*", attr.Value) +} + func testDeleteUser(ta TestAccessor, t *testing.T) { t.Log("TestDeleteUser") ta.Truncate() @@ -319,7 +384,7 @@ func testUpdateUser(ta TestAccessor, t *testing.T) { func testInsertAndGetAffiliation(ta TestAccessor, t *testing.T) { ta.Truncate() - err := ta.Accessor.InsertAffiliation("Bank1", "Banks") + err := ta.Accessor.InsertAffiliation("Bank1", "Banks", 0) if err != nil { t.Errorf("Error occured during insert query of group: %s, error: %s", "Bank1", err) } @@ -338,7 +403,7 @@ func testInsertAndGetAffiliation(ta TestAccessor, t *testing.T) { func testDeleteAffiliation(ta TestAccessor, t *testing.T) { ta.Truncate() - err := ta.Accessor.InsertAffiliation("Banks.Bank2", "Banks") + err := ta.Accessor.InsertAffiliation("Banks.Bank2", "Banks", 0) if err != nil { t.Errorf("Error occured during insert query of group: %s, error: %s", "Bank2", err) } @@ -372,8 +437,7 @@ func TestDBErrorMessages(t *testing.T) { t.Error("Failed to open connection to DB") } - accessor := NewDBAccessor() - accessor.SetDB(db) + accessor := NewDBAccessor(db) ta := TestAccessor{ Accessor: accessor, @@ -391,7 +455,7 @@ func TestDBErrorMessages(t *testing.T) { assert.Contains(t, err.Error(), fmt.Sprintf(expectedErr, "User")) } - newCertDBAcc := NewCertDBAccessor(db) + newCertDBAcc := NewCertDBAccessor(db, 0) _, err = newCertDBAcc.GetCertificateWithID("serial", "aki") if assert.Error(t, err, "Should have errored, and not returned any results") { assert.Contains(t, err.Error(), fmt.Sprintf(expectedErr, "Certificate")) diff --git a/lib/dbaccessor.go b/lib/dbaccessor.go index 1c0b1e04b..cc4e5992b 100644 --- a/lib/dbaccessor.go +++ b/lib/dbaccessor.go @@ -39,8 +39,8 @@ func init() { const ( insertUser = ` -INSERT INTO users (id, token, type, affiliation, attributes, state, max_enrollments) - VALUES (:id, :token, :type, :affiliation, :attributes, :state, :max_enrollments);` +INSERT INTO users (id, token, type, affiliation, attributes, state, max_enrollments, level) + VALUES (:id, :token, :type, :affiliation, :attributes, :state, :max_enrollments, :level);` deleteUser = ` DELETE FROM users @@ -48,7 +48,7 @@ DELETE FROM users updateUser = ` UPDATE users - SET token = :token, type = :type, affiliation = :affiliation, attributes = :attributes, state = :state + SET token = :token, type = :type, affiliation = :affiliation, attributes = :attributes, state = :state, level = :level WHERE (id = :id);` getUser = ` @@ -56,15 +56,15 @@ SELECT * FROM users WHERE (id = ?)` insertAffiliation = ` -INSERT INTO affiliations (name, prekey) - VALUES (?, ?)` +INSERT INTO affiliations (name, prekey, level) + VALUES (?, ?, ?)` deleteAffiliation = ` DELETE FROM affiliations WHERE (name = ?)` getAffiliation = ` -SELECT name, prekey FROM affiliations +SELECT * FROM affiliations WHERE (name = ?)` ) @@ -77,6 +77,14 @@ type UserRecord struct { Attributes string `db:"attributes"` State int `db:"state"` MaxEnrollments int `db:"max_enrollments"` + Level int `db:"level"` +} + +// AffiliationRecord defines the properties of an affiliation +type AffiliationRecord struct { + Name string `db:"name"` + Prekey string `db:"prekey"` + Level int `db:"level"` } // Accessor implements db.Accessor interface. @@ -85,8 +93,10 @@ type Accessor struct { } // NewDBAccessor is a constructor for the database API -func NewDBAccessor() *Accessor { - return &Accessor{} +func NewDBAccessor(db *sqlx.DB) *Accessor { + return &Accessor{ + db: db, + } } func (d *Accessor) checkDB() error { @@ -131,6 +141,7 @@ func (d *Accessor) InsertUser(user spi.UserInfo) error { Attributes: string(attrBytes), State: user.State, MaxEnrollments: user.MaxEnrollments, + Level: user.Level, }) if err != nil { @@ -201,6 +212,7 @@ func (d *Accessor) UpdateUser(user spi.UserInfo) error { Attributes: string(attributes), State: user.State, MaxEnrollments: user.MaxEnrollments, + Level: user.Level, }) if err != nil { @@ -240,7 +252,7 @@ func (d *Accessor) GetUser(id string, attrs []string) (spi.User, error) { } // InsertAffiliation inserts affiliation into database -func (d *Accessor) InsertAffiliation(name string, prekey string) error { +func (d *Accessor) InsertAffiliation(name string, prekey string, level int) error { log.Debugf("DB: Add affiliation %s", name) err := d.checkDB() if err != nil { @@ -259,7 +271,7 @@ func (d *Accessor) InsertAffiliation(name string, prekey string) error { return nil } } - _, err = d.db.Exec(d.db.Rebind(insertAffiliation), name, prekey) + _, err = d.db.Exec(d.db.Rebind(insertAffiliation), name, prekey, level) if err != nil { if (!strings.Contains(err.Error(), "UNIQUE constraint failed") && dbType == "sqlite3") || (!strings.Contains(err.Error(), "duplicate key value") && dbType == "postgres") { return err @@ -296,14 +308,72 @@ func (d *Accessor) GetAffiliation(name string) (spi.Affiliation, error) { return nil, err } - var affiliation spi.AffiliationImpl - - err = d.db.Get(&affiliation, d.db.Rebind(getAffiliation), name) + var affiliationRecord AffiliationRecord + err = d.db.Get(&affiliationRecord, d.db.Rebind(getAffiliation), name) if err != nil { return nil, dbGetError(err, "Affiliation") } - return &affiliation, nil + affiliation := spi.NewAffiliation(affiliationRecord.Name, affiliationRecord.Prekey, affiliationRecord.Level) + + return affiliation, nil +} + +// GetProperties returns the properties from the database +func (d *Accessor) GetProperties(names []string) (map[string]string, error) { + log.Debugf("DB: Get properties %s", names) + err := d.checkDB() + if err != nil { + return nil, err + } + + type property struct { + Name string `db:"property"` + Value string `db:"value"` + } + + properties := []property{} + + query := "SELECT * FROM properties WHERE (property IN (?))" + inQuery, args, err := sqlx.In(query, names) + if err != nil { + return nil, errors.Wrapf(err, "Failed to construct query '%s' for properties '%s'", query, names) + } + err = d.db.Select(&properties, d.db.Rebind(inQuery), args...) + if err != nil { + return nil, dbGetError(err, "Properties") + } + + propertiesMap := make(map[string]string) + for _, prop := range properties { + propertiesMap[prop.Name] = prop.Value + } + + return propertiesMap, nil +} + +// GetUserLessThanLevel returns all identities that are less than the level specified +// Otherwise, returns no users if requested level is zero +func (d *Accessor) GetUserLessThanLevel(level int) ([]spi.User, error) { + var users []UserRecord + + if level == 0 { + return []spi.User{}, nil + } + + err := d.db.Select(&users, d.db.Rebind("SELECT * FROM users WHERE (level < ?) OR (level IS NULL)"), level) + if err != nil { + return nil, errors.Wrap(err, "Failed to get identities that need to be updated") + } + + allUsers := []spi.User{} + + for _, user := range users { + dbUser := d.newDBUser(&user) + allUsers = append(allUsers, dbUser) + } + + return allUsers, nil } // Creates a DBUser object from the DB user record @@ -315,6 +385,7 @@ func (d *Accessor) newDBUser(userRec *UserRecord) *DBUser { user.MaxEnrollments = userRec.MaxEnrollments user.Affiliation = userRec.Affiliation user.Type = userRec.Type + user.Level = userRec.Level var attrs []api.Attribute json.Unmarshal([]byte(userRec.Attributes), &attrs) @@ -356,6 +427,34 @@ func (u *DBUser) GetMaxEnrollments() int { return u.MaxEnrollments } +// GetLevel returns the level of the user +func (u *DBUser) GetLevel() int { + return u.Level +} + +// SetLevel sets the level of the user +func (u *DBUser) SetLevel(level int) error { + query := "UPDATE users SET level = ? where (id = ?)" + id := u.GetName() + res, err := u.db.Exec(u.db.Rebind(query), level, id) + if err != nil { + return err + } + numRowsAffected, err := res.RowsAffected() + if err != nil { + return errors.Wrap(err, "Failed to get number of rows affected") + } + + if numRowsAffected == 0 { + return errors.Errorf("No rows were affected when updating the state of identity %s", id) + } + + if numRowsAffected != 1 { + return errors.Errorf("%d rows were affected when updating the state of identity %s", numRowsAffected, id) + } + return nil +} + // Login the user with a password func (u *DBUser) Login(pass string, caMaxEnrollments int) error { log.Debugf("DB: Login user %s with max enrollments of %d and state of %d", u.Name, u.MaxEnrollments, u.State) @@ -495,6 +594,64 @@ func (u *DBUser) Revoke() error { return nil } +// ModifyAttributes adds a new attribute, modifies existing attribute, or delete attribute +func (u *DBUser) ModifyAttributes(attrs []api.Attribute) error { + log.Debugf("Modify Attributes: %+v", attrs) + userAttrs, _ := u.GetAttributes(nil) + var attr api.Attribute + for _, attr = range attrs { + log.Debugf("Attribute request: %+v", attr) + found := false + for i := range userAttrs { + if userAttrs[i].Name == attr.Name { + if attr.Value == "" { + log.Debugf("Deleting attribute: %+v", userAttrs[i]) + if i == len(userAttrs)-1 { + userAttrs = userAttrs[:len(userAttrs)-1] + } else { + userAttrs = append(userAttrs[:i], userAttrs[i+1:]...) + } + } else { + log.Debugf("Updating existing attribute from '%+v' to '%+v'", userAttrs[i], attr) + userAttrs[i].Value = attr.Value + } + found = true + break + } + } + if !found && attr.Value != "" { + log.Debugf("Adding '%+v' as new attribute", attr) + userAttrs = append(userAttrs, attr) + } + } + + attrBytes, err := json.Marshal(userAttrs) + if err != nil { + return err + } + + query := "UPDATE users SET attributes = ? where (id = ?)" + id := u.GetName() + res, err := u.db.Exec(u.db.Rebind(query), string(attrBytes), id) + if err != nil { + return err + } + + numRowsAffected, err := res.RowsAffected() + if err != nil { + return errors.Wrap(err, "Failed to get number of rows affected") + } + + if numRowsAffected == 0 { + return errors.Errorf("No rows were affected when updating the state of identity %s", id) + } + + if numRowsAffected != 1 { + return errors.Errorf("%d rows were affected when updating the state of identity %s", numRowsAffected, id) + } + return nil +} + func dbGetError(err error, prefix string) error { if err.Error() == "sql: no rows in result set" { return errors.Errorf("%s not found", prefix) diff --git a/lib/dbutil/dbutil.go b/lib/dbutil/dbutil.go index 351edf9db..37c849873 100644 --- a/lib/dbutil/dbutil.go +++ b/lib/dbutil/dbutil.go @@ -35,6 +35,13 @@ var ( dbURLRegex = regexp.MustCompile("(Datasource:\\s*)?(\\S+):(\\S+)@|(Datasource:.*\\s)?(user=\\S+).*\\s(password=\\S+)|(Datasource:.*\\s)?(password=\\S+).*\\s(user=\\S+)") ) +// Levels contains the levels of identities, affiliations, and certificates +type Levels struct { + Identity int + Affiliation int + Certificate int +} + // NewUserRegistrySQLLite3 returns a pointer to a sqlite database func NewUserRegistrySQLLite3(datasource string) (*sqlx.DB, error) { log.Debugf("Using sqlite database, connect to database in home (%s) directory", datasource) @@ -74,18 +81,27 @@ func createSQLiteDBTables(datasource string) error { defer db.Close() log.Debug("Creating users table if it does not exist") - if _, err := db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(255), token bytea, type VARCHAR(256), affiliation VARCHAR(1024), attributes TEXT, state INTEGER, max_enrollments INTEGER)"); err != nil { + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(255), token bytea, type VARCHAR(256), affiliation VARCHAR(1024), attributes TEXT, state INTEGER, max_enrollments INTEGER, level INTEGER DEFAULT 0)"); err != nil { return errors.Wrap(err, "Error creating users table") } log.Debug("Creating affiliations table if it does not exist") - if _, err := db.Exec("CREATE TABLE IF NOT EXISTS affiliations (name VARCHAR(1024) NOT NULL UNIQUE, prekey VARCHAR(1024))"); err != nil { + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS affiliations (name VARCHAR(1024) NOT NULL UNIQUE, prekey VARCHAR(1024), level INTEGER DEFAULT 0)"); err != nil { return errors.Wrap(err, "Error creating affiliations table") } log.Debug("Creating certificates table if it does not exist") - if _, err := db.Exec("CREATE TABLE IF NOT EXISTS certificates (id VARCHAR(255), serial_number blob NOT NULL, authority_key_identifier blob NOT NULL, ca_label blob, status blob NOT NULL, reason int, expiry timestamp, revoked_at timestamp, pem blob NOT NULL, PRIMARY KEY(serial_number, authority_key_identifier))"); err != nil { + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS certificates (id VARCHAR(255), serial_number blob NOT NULL, authority_key_identifier blob NOT NULL, ca_label blob, status blob NOT NULL, reason int, expiry timestamp, revoked_at timestamp, pem blob NOT NULL, level INTEGER DEFAULT 0, PRIMARY KEY(serial_number, authority_key_identifier))"); err != nil { return errors.Wrap(err, "Error creating certificates table") } - + log.Debug("Creating properties table if it does not exist") + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS properties (property VARCHAR(255), value VARCHAR(256), PRIMARY KEY(property))"); err != nil { + return errors.Wrap(err, "Error creating properties table") + } + _, err = db.Exec(db.Rebind("INSERT INTO properties (property, value) VALUES ('identity.level', '0'), ('affiliation.level', '0'), ('certificate.level', '0')")) + if err != nil { + if !strings.Contains(err.Error(), "UNIQUE constraint failed") { + return errors.Wrap(err, "Failed to initialize properties table") + } + } return nil } @@ -171,17 +187,27 @@ func createPostgresDatabase(dbName string, db *sqlx.DB) error { // createPostgresDB creates postgres database func createPostgresTables(dbName string, db *sqlx.DB) error { log.Debug("Creating users table if it does not exist") - if _, err := db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(255), token bytea, type VARCHAR(256), affiliation VARCHAR(1024), attributes TEXT, state INTEGER, max_enrollments INTEGER)"); err != nil { + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(255), token bytea, type VARCHAR(256), affiliation VARCHAR(1024), attributes TEXT, state INTEGER, max_enrollments INTEGER, level INTEGER DEFAULT 0)"); err != nil { return errors.Wrap(err, "Error creating users table") } log.Debug("Creating affiliations table if it does not exist") - if _, err := db.Exec("CREATE TABLE IF NOT EXISTS affiliations (name VARCHAR(1024) NOT NULL UNIQUE, prekey VARCHAR(1024))"); err != nil { + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS affiliations (name VARCHAR(1024) NOT NULL UNIQUE, prekey VARCHAR(1024), level INTEGER DEFAULT 0)"); err != nil { return errors.Wrap(err, "Error creating affiliations table") } log.Debug("Creating certificates table if it does not exist") - if _, err := db.Exec("CREATE TABLE IF NOT EXISTS certificates (id VARCHAR(255), serial_number bytea NOT NULL, authority_key_identifier bytea NOT NULL, ca_label bytea, status bytea NOT NULL, reason int, expiry timestamp, revoked_at timestamp, pem bytea NOT NULL, PRIMARY KEY(serial_number, authority_key_identifier))"); err != nil { + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS certificates (id VARCHAR(255), serial_number bytea NOT NULL, authority_key_identifier bytea NOT NULL, ca_label bytea, status bytea NOT NULL, reason int, expiry timestamp, revoked_at timestamp, pem bytea NOT NULL, level INTEGER DEFAULT 0, PRIMARY KEY(serial_number, authority_key_identifier))"); err != nil { return errors.Wrap(err, "Error creating certificates table") } + log.Debug("Creating properties table if it does not exist") + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS properties (property VARCHAR(255), value VARCHAR(256), PRIMARY KEY(property))"); err != nil { + return errors.Wrap(err, "Error creating properties table") + } + _, err := db.Exec(db.Rebind("INSERT INTO properties (property, value) VALUES ('identity.level', '0'), ('affiliation.level', '0'), ('certificate.level', '0')")) + if err != nil { + if !strings.Contains(err.Error(), "duplicate key") { + return err + } + } return nil } @@ -247,27 +273,33 @@ func createMySQLDatabase(dbName string, db *sqlx.DB) error { func createMySQLTables(dbName string, db *sqlx.DB) error { log.Debug("Creating users table if it doesn't exist") - if _, err := db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(255) NOT NULL, token blob, type VARCHAR(256), affiliation VARCHAR(1024), attributes TEXT, state INTEGER, max_enrollments INTEGER, PRIMARY KEY (id)) DEFAULT CHARSET=utf8 COLLATE utf8_bin"); err != nil { + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS users (id VARCHAR(255) NOT NULL, token blob, type VARCHAR(256), affiliation VARCHAR(1024), attributes TEXT, state INTEGER, max_enrollments INTEGER, level INTEGER DEFAULT 0, PRIMARY KEY (id)) DEFAULT CHARSET=utf8 COLLATE utf8_bin"); err != nil { return errors.Wrap(err, "Error creating users table") } - log.Debug("Creating affiliations table if it doesn't exist") - if _, err := db.Exec("CREATE TABLE IF NOT EXISTS affiliations (name VARCHAR(1024) NOT NULL, prekey VARCHAR(1024))"); err != nil { + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS affiliations (name VARCHAR(1024) NOT NULL, prekey VARCHAR(1024), level INTEGER DEFAULT 0)"); err != nil { return errors.Wrap(err, "Error creating affiliations table") } - log.Debug("Creating index on 'name' in the affiliations table") if _, err := db.Exec("CREATE INDEX name_index on affiliations (name)"); err != nil { if !strings.Contains(err.Error(), "Error 1061") { // Error 1061: Duplicate key name, index already exists return errors.Wrap(err, "Error creating index on affiliations table") } } - log.Debug("Creating certificates table if it doesn't exist") - if _, err := db.Exec("CREATE TABLE IF NOT EXISTS certificates (id VARCHAR(255), serial_number varbinary(128) NOT NULL, authority_key_identifier varbinary(128) NOT NULL, ca_label varbinary(128), status varbinary(128) NOT NULL, reason int, expiry timestamp DEFAULT 0, revoked_at timestamp DEFAULT 0, pem varbinary(4096) NOT NULL, PRIMARY KEY(serial_number, authority_key_identifier)) DEFAULT CHARSET=utf8 COLLATE utf8_bin"); err != nil { + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS certificates (id VARCHAR(255), serial_number varbinary(128) NOT NULL, authority_key_identifier varbinary(128) NOT NULL, ca_label varbinary(128), status varbinary(128) NOT NULL, reason int, expiry timestamp DEFAULT 0, revoked_at timestamp DEFAULT 0, pem varbinary(4096) NOT NULL, level INTEGER DEFAULT 0, PRIMARY KEY(serial_number, authority_key_identifier)) DEFAULT CHARSET=utf8 COLLATE utf8_bin"); err != nil { return errors.Wrap(err, "Error creating certificates table") } - + log.Debug("Creating properties table if it does not exist") + if _, err := db.Exec("CREATE TABLE IF NOT EXISTS properties (property VARCHAR(255), value VARCHAR(256), PRIMARY KEY(property))"); err != nil { + return errors.Wrap(err, "Error creating properties table") + } + _, err := db.Exec(db.Rebind("INSERT INTO properties (property, value) VALUES ('identity.level', '0'), ('affiliation.level', '0'), ('certificate.level', '0')")) + if err != nil { + if !strings.Contains(err.Error(), "1062") { // MySQL error code for duplicate entry + return err + } + } return nil } @@ -349,6 +381,26 @@ func UpdateSchema(db *sqlx.DB) error { } } +// UpdateDBLevel updates the levels for the tables in the database +func UpdateDBLevel(db *sqlx.DB, levels *Levels) error { + log.Debugf("Updating database level to %+v", levels) + + _, err := db.Exec(db.Rebind("UPDATE properties SET value = ? WHERE (property = 'identity.level')"), levels.Identity) + if err != nil { + return err + } + _, err = db.Exec(db.Rebind("UPDATE properties SET value = ? WHERE (property = 'affiliation.level')"), levels.Affiliation) + if err != nil { + return err + } + _, err = db.Exec(db.Rebind("UPDATE properties SET value = ? WHERE (property = 'certificate.level')"), levels.Certificate) + if err != nil { + return err + } + + return nil +} + func updateMySQLSchema(db *sqlx.DB) error { log.Debug("Update MySQL schema if using outdated schema") var err error @@ -361,6 +413,24 @@ func updateMySQLSchema(db *sqlx.DB) error { if err != nil { return err } + _, err = db.Exec("ALTER TABLE users ADD COLUMN level INTEGER DEFAULT 0 AFTER max_enrollments") + if err != nil { + if !strings.Contains(err.Error(), "1060") { // Already using the latest schema + return err + } + } + _, err = db.Exec("ALTER TABLE certificates ADD COLUMN level INTEGER DEFAULT 0 AFTER pem") + if err != nil { + if !strings.Contains(err.Error(), "1060") { // Already using the latest schema + return err + } + } + _, err = db.Exec("ALTER TABLE affiliations ADD COLUMN level INTEGER DEFAULT 0 AFTER prekey") + if err != nil { + if !strings.Contains(err.Error(), "1060") { // Already using the latest schema + return err + } + } _, err = db.Exec("ALTER TABLE affiliations DROP INDEX name;") if err != nil { if !strings.Contains(err.Error(), "Error 1091") { // Indicates that index not found @@ -397,6 +467,24 @@ func updatePostgresSchema(db *sqlx.DB) error { if err != nil { return err } + _, err = db.Exec("ALTER TABLE users ADD COLUMN level INTEGER DEFAULT 0") + if err != nil { + if !strings.Contains(err.Error(), "already exists") { + return err + } + } + _, err = db.Exec("ALTER TABLE certificates ADD COLUMN level INTEGER DEFAULT 0") + if err != nil { + if !strings.Contains(err.Error(), "already exists") { + return err + } + } + _, err = db.Exec("ALTER TABLE affiliations ADD COLUMN level INTEGER DEFAULT 0") + if err != nil { + if !strings.Contains(err.Error(), "already exists") { + return err + } + } _, err = db.Exec("ALTER TABLE affiliations ALTER COLUMN name TYPE VARCHAR(1024), ALTER COLUMN prekey TYPE VARCHAR(1024)") if err != nil { return err diff --git a/lib/ldap/client.go b/lib/ldap/client.go index f0d57dffc..d14b9f64c 100644 --- a/lib/ldap/client.go +++ b/lib/ldap/client.go @@ -242,7 +242,7 @@ func (lc *Client) GetRootAffiliation() (spi.Affiliation, error) { } // InsertAffiliation adds an affiliation group -func (lc *Client) InsertAffiliation(name string, prekey string) error { +func (lc *Client) InsertAffiliation(name string, prekey string, version int) error { return errNotSupported } @@ -251,6 +251,11 @@ func (lc *Client) DeleteAffiliation(name string) error { return errNotSupported } +// GetProperties returns the properties from the database +func (lc *Client) GetProperties(name []string) (map[string]string, error) { + return nil, errNotSupported +} + // Connect to the LDAP server and bind as user as admin user as specified in LDAP URL func (lc *Client) newConnection() (conn *ldap.Conn, err error) { address := fmt.Sprintf("%s:%d", lc.Host, lc.Port) @@ -308,6 +313,11 @@ func (u *User) GetMaxEnrollments() int { return 0 } +// GetLevel returns the level of the user +func (u *User) GetLevel() int { + return 0 +} + // Login logs a user in using password func (u *User) Login(password string, caMaxEnrollment int) error { @@ -375,6 +385,11 @@ func (u *User) Revoke() error { return nil } +// ModifyAttributes adds a new attribute or modifies existing attribute +func (u *User) ModifyAttributes(attrs []api.Attribute) error { + return errNotSupported +} + // Returns a slice with the elements reversed func reverse(in []string) []string { size := len(in) diff --git a/lib/metadata/version.go b/lib/metadata/version.go new file mode 100644 index 000000000..da3a92dfe --- /dev/null +++ b/lib/metadata/version.go @@ -0,0 +1,142 @@ +/* +Copyright IBM Corp. 2017 All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metadata + +import ( + "fmt" + "runtime" + "strconv" + "strings" + + "github.com/hyperledger/fabric-ca/lib/dbutil" + "github.com/pkg/errors" +) + +// Current levels which are incremented each time there is a change which +// requires database migration +const ( + // IdentityLevel is the current level of identities + IdentityLevel = 1 + // AffiliationLevel is the current level of affiliations + AffiliationLevel = 0 + // CertificateLevel is the current level of certificates + CertificateLevel = 0 +) + +// Version specifies fabric-ca-client/fabric-ca-server version +// It is defined by the Makefile and passed in with ldflags +var Version string + +// GetVersionInfo returns version information for the fabric-ca-client/fabric-ca-server +func GetVersionInfo(prgName string) string { + if Version == "" { + Version = "development build" + } + + return fmt.Sprintf("%s:\n Version: %s\n Go version: %s\n OS/Arch: %s\n", + prgName, Version, runtime.Version(), + fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)) +} + +// GetVersion returns the version +func GetVersion() string { + if Version == "" { + panic("Version is not set for fabric-ca library") + } + return Version +} + +// Mapping of versions to levels. +// NOTE: Append new versions to this array if migration is +// required for identity, affiliation, or certificate information. +var versionToLevelsMapping = []versionLevels{ + { + version: "0", + levels: &dbutil.Levels{Identity: 0, Affiliation: 0, Certificate: 0}, + }, + { + version: "1.1.0", + levels: &dbutil.Levels{Identity: 1, Affiliation: 0, Certificate: 0}, + }, +} + +type versionLevels struct { + version string + levels *dbutil.Levels +} + +// GetLevels returns the levels for a particular version +func GetLevels(version string) (*dbutil.Levels, error) { + for i := len(versionToLevelsMapping) - 1; i >= 0; i-- { + vl := versionToLevelsMapping[i] + cmp, err := CmpVersion(vl.version, version) + if err != nil { + return nil, err + } + if cmp >= 0 { + return vl.levels, nil + } + } + return nil, nil +} + +// CmpVersion compares version v1 to v2. +// Return 0 if equal, 1 if v2 > v1, or -1 if v2 < v1. +func CmpVersion(v1, v2 string) (int, error) { + v1strs := strs(v1) + v2strs := strs(v2) + m := max(len(v1strs), len(v2strs)) + for i := 0; i < m; i++ { + v1val, err := val(v1strs, i) + if err != nil { + return 0, errors.WithMessage(err, fmt.Sprintf("Invalid version: '%s'", v1)) + } + v2val, err := val(v2strs, i) + if err != nil { + return 0, errors.WithMessage(err, fmt.Sprintf("Invalid version: '%s'", v2)) + } + if v1val < v2val { + return 1, nil + } else if v1val > v2val { + return -1, nil + } + } + return 0, nil +} + +func strs(version string) []string { + return strings.Split(strings.Split(version, "-")[0], ".") +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func val(strs []string, i int) (int, error) { + if i >= len(strs) { + return 0, nil + } + str := strs[i] + v, err := strconv.Atoi(str) + if err != nil { + return 0, errors.WithMessage(err, fmt.Sprintf("Invalid version format at '%s'", str)) + } + return v, nil +} diff --git a/lib/metadata/version_test.go b/lib/metadata/version_test.go new file mode 100644 index 000000000..6443a7565 --- /dev/null +++ b/lib/metadata/version_test.go @@ -0,0 +1,84 @@ +/* +Copyright IBM Corp. 2016 All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metadata_test + +import ( + "testing" + + "github.com/hyperledger/fabric-ca/lib/metadata" + "github.com/stretchr/testify/assert" +) + +func TestVersion(t *testing.T) { + // Positive test cases + cmpVersion(t, "1.1.1-xxxx", "1.1.1-yyy-zzz", 0) + cmpVersion(t, "1.0.0", "1.1.0", 1) + cmpVersion(t, "1.1.0", "1.1.0.0.0", 0) + cmpVersion(t, "1.5.0.0.0", "1.5", 0) + cmpVersion(t, "1.0.0", "1.0.0.1", 1) + cmpVersion(t, "1.1.0", "1.0.0", -1) + cmpVersion(t, "1.0.0.0.1", "1.0", -1) + cmpLevels(t, "1.0.0", 0, 0, 0) + cmpLevels(t, "1.0.4", 0, 0, 0) + cmpLevels(t, "1.1.0", 1, 0, 0) + cmpLevels(t, "1.1.1", 1, 0, 0) + cmpLevels(t, "1.2.1", 1, 0, 0) + // Negative test cases + _, err := metadata.CmpVersion("1.x.2.0", "1.7.8") + if err == nil { + t.Error("Expecting error at 1.x.2.0") + } + _, err = metadata.CmpVersion("1.2.0", "x.1.7.8") + if err == nil { + t.Error("Expecting error at x.1.7.8") + } +} + +func cmpVersion(t *testing.T, v1, v2 string, expectedResult int) { + result, err := metadata.CmpVersion(v1, v2) + if err != nil { + t.Fatalf("Failed comparing versions: %s", err) + } + assert.Equal(t, expectedResult, result) +} + +func cmpLevels(t *testing.T, version string, identity, affiliation, certificate int) { + levels, err := metadata.GetLevels(version) + if err != nil { + t.Fatalf("GetLevels failed: %s", err) + } + assert.Equal(t, levels.Identity, identity) + assert.Equal(t, levels.Affiliation, affiliation) + assert.Equal(t, levels.Certificate, certificate) +} + +func TestGetVersionInfo(t *testing.T) { + info := metadata.GetVersionInfo("fabric-ca-client") + assert.Contains(t, info, "Version: development build") + + metadata.Version = "1.0.0" + info = metadata.GetVersionInfo("fabric-ca-client") + assert.Contains(t, info, "Version: 1.0.0") +} + +func TestGetVersion(t *testing.T) { + info := metadata.GetVersion() + + metadata.Version = "1.0.0" + info = metadata.GetVersion() + assert.Contains(t, info, "1.0.0") +} diff --git a/lib/server.go b/lib/server.go index 3ce71d36c..412b87389 100644 --- a/lib/server.go +++ b/lib/server.go @@ -39,6 +39,7 @@ import ( "github.com/cloudflare/cfssl/revoke" "github.com/cloudflare/cfssl/signer" "github.com/hyperledger/fabric-ca/lib/dbutil" + "github.com/hyperledger/fabric-ca/lib/metadata" stls "github.com/hyperledger/fabric-ca/lib/tls" "github.com/hyperledger/fabric-ca/util" "github.com/spf13/viper" @@ -51,7 +52,8 @@ import ( const ( defaultClientAuth = "noclientcert" fabricCAServerProfilePort = "FABRIC_CA_SERVER_PROFILE_PORT" - allRoles = "user,app,peer,orderer,client,validator,auditor" + allRoles = "peer,orderer,client,user" + apiPathPrefix = "/api/v1/" ) // Attribute names @@ -62,7 +64,6 @@ const ( attrIntermediateCA = "hf.IntermediateCA" attrGenCRL = "hf.GenCRL" attrRegistrarAttr = "hf.Registrar.Attributes" - apiPathPrefix = "/api/v1/" ) // Server is the fabric-ca server @@ -90,6 +91,8 @@ type Server struct { wait chan bool // Server mutex mutex sync.Mutex + // The server's current levels + levels *dbutil.Levels } // Init initializes a fabric-ca server @@ -104,6 +107,7 @@ func (s *Server) Init(renew bool) (err error) { // init initializses the server leaving the DB open func (s *Server) init(renew bool) (err error) { + log.Debugf("Server Version: %s", metadata.GetVersion()) // Initialize the config err = s.initConfig() if err != nil { @@ -231,16 +235,13 @@ func (s *Server) initConfig() (err error) { if err != nil { return errors.Wrap(err, "Failed to get server's home directory") } - } - // Make home directory absolute, if not already absoluteHomeDir, err := filepath.Abs(s.HomeDir) if err != nil { return fmt.Errorf("Failed to make server's home directory path absolute: %s", err) } s.HomeDir = absoluteHomeDir - // Create config if not set if s.Config == nil { s.Config = new(ServerConfig) @@ -252,10 +253,6 @@ func (s *Server) initConfig() (err error) { } s.CA.server = s s.CA.HomeDir = s.HomeDir - err = s.CA.initConfig() - if err != nil { - return err - } err = s.initMultiCAConfig() if err != nil { return err diff --git a/lib/server_test.go b/lib/server_test.go index 757ce9d9c..e978e748b 100755 --- a/lib/server_test.go +++ b/lib/server_test.go @@ -37,6 +37,7 @@ import ( "github.com/hyperledger/fabric-ca/api" . "github.com/hyperledger/fabric-ca/lib" "github.com/hyperledger/fabric-ca/lib/dbutil" + "github.com/hyperledger/fabric-ca/lib/metadata" libtls "github.com/hyperledger/fabric-ca/lib/tls" "github.com/hyperledger/fabric-ca/util" "github.com/hyperledger/fabric/bccsp/factory" @@ -53,6 +54,11 @@ const ( pportEnvVar = "FABRIC_CA_SERVER_PROFILE_PORT" ) +func TestMain(m *testing.M) { + metadata.Version = "1.1.0" + os.Exit(m.Run()) +} + func TestSRVServerInit(t *testing.T) { server := TestGetRootServer(t) if server == nil { diff --git a/lib/server_whitebox_test.go b/lib/server_whitebox_test.go index 2c12a73e4..4155eac0d 100644 --- a/lib/server_whitebox_test.go +++ b/lib/server_whitebox_test.go @@ -17,8 +17,6 @@ package lib import ( "testing" - - "github.com/hyperledger/fabric-ca/lib/spi" ) const ( @@ -51,7 +49,7 @@ func TestGetAffliation(t *testing.T) { } defer srv.Stop() - afs := []spi.AffiliationImpl{} + afs := []AffiliationRecord{} err = srv.db.Select(&afs, srv.db.Rebind(getAffiliation), affiliationName) t.Logf("Retrieved %+v for the affiliation %s", afs, affiliationName) if err != nil { diff --git a/lib/servererror.go b/lib/servererror.go index b608c3a50..457df3c46 100644 --- a/lib/servererror.go +++ b/lib/servererror.go @@ -123,6 +123,10 @@ const ( ErrGettingType = 45 // CA cert does not have 'crl sign' usage ErrNoCrlSignAuth = 46 + // Incorrect level of database + ErrDBLevel = 47 + // Incorrect level of configuration file + ErrConfigFileLevel = 48 ) // Construct a new HTTP error. diff --git a/lib/spi/affiliation.go b/lib/spi/affiliation.go index ed6bb7323..e7bc99b2a 100644 --- a/lib/spi/affiliation.go +++ b/lib/spi/affiliation.go @@ -16,24 +16,40 @@ limitations under the License. package spi -// AffiliationImpl defines a group name and its parent -type AffiliationImpl struct { +// affiliationImpl defines a group name and its parent +type affiliationImpl struct { Name string `db:"name"` Prekey string `db:"prekey"` + Level int `db:"level"` } // Affiliation is the API for a user's affiliation type Affiliation interface { GetName() string GetPrekey() string + GetLevel() int +} + +// NewAffiliation returns an affiliationImpl object +func NewAffiliation(name, prekey string, level int) Affiliation { + return &affiliationImpl{ + Name: name, + Prekey: prekey, + Level: level, + } } // GetName returns the name of the affiliation -func (g *AffiliationImpl) GetName() string { +func (g *affiliationImpl) GetName() string { return g.Name } // GetPrekey returns the prekey of the affiliation -func (g *AffiliationImpl) GetPrekey() string { +func (g *affiliationImpl) GetPrekey() string { return g.Prekey } + +// GetLevel returns the level of the affiliation +func (g *affiliationImpl) GetLevel() int { + return g.Level +} diff --git a/lib/spi/affiliation_test.go b/lib/spi/affiliation_test.go index 801dd50cc..c4880c9d3 100644 --- a/lib/spi/affiliation_test.go +++ b/lib/spi/affiliation_test.go @@ -19,7 +19,7 @@ package spi import "testing" func TestGetName(t *testing.T) { - aff := &AffiliationImpl{Name: "Bank_a", Prekey: "1234"} + aff := NewAffiliation("Bank_a", "1234", 0) name := aff.GetName() if name != "Bank_a" { @@ -28,10 +28,19 @@ func TestGetName(t *testing.T) { } func TestGetPrekey(t *testing.T) { - aff := &AffiliationImpl{Name: "Bank_a", Prekey: "1234"} + aff := NewAffiliation("Bank_a", "1234", 0) name := aff.GetPrekey() if name != "1234" { t.Error("Prekey does not match, expected '1234'") } } + +func TestGetLevel(t *testing.T) { + aff := NewAffiliation("Bank_a", "1234", 2) + level := aff.GetLevel() + + if level != 2 { + t.Error("Level does not match, expected '2'") + } +} diff --git a/lib/spi/userregistry.go b/lib/spi/userregistry.go index a3e885d98..57c4c2212 100644 --- a/lib/spi/userregistry.go +++ b/lib/spi/userregistry.go @@ -33,6 +33,7 @@ type UserInfo struct { Attributes []api.Attribute State int MaxEnrollments int + Level int } // User is the SPI for a user @@ -51,10 +52,14 @@ type User interface { GetAttribute(name string) (*api.Attribute, error) // GetAttributes returns the requested attributes GetAttributes(attrNames []string) ([]api.Attribute, error) + // ModifyAttributes adds a new attribute or modifies existing attribute + ModifyAttributes(attrs []api.Attribute) error // LoginComplete completes the login process by incrementing the state of the user LoginComplete() error // Revoke will revoke the user, setting the state of the user to be -1 Revoke() error + // GetLevel returns the level of the user, level is used to verify if the user needs migration + GetLevel() int } // UserRegistry is the API for retreiving users and groups @@ -64,6 +69,8 @@ type UserRegistry interface { UpdateUser(user UserInfo) error DeleteUser(id string) error GetAffiliation(name string) (Affiliation, error) - InsertAffiliation(name string, prekey string) error + InsertAffiliation(name string, prekey string, level int) error DeleteAffiliation(name string) error + // GetProperties returns the properties by name from the database + GetProperties(name []string) (map[string]string, error) } diff --git a/scripts/fvt/backwards_comp_test.sh b/scripts/fvt/backwards_comp_test.sh new file mode 100755 index 000000000..626695451 --- /dev/null +++ b/scripts/fvt/backwards_comp_test.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +TESTCASE="backwards_comp" +FABRIC_CA="$GOPATH/src/github.com/hyperledger/fabric-ca" +SCRIPTDIR="$FABRIC_CA/scripts/fvt" +. $SCRIPTDIR/fabric-ca_utils +RC=0 + +export FABRIC_CA_SERVER_HOME="/tmp/$TESTCASE" +export CA_CFG_PATH="/tmp/$TESTCASE" + +TESTCONFIG="$FABRIC_CA_SERVER_HOME/testconfig.yaml" + +function genConfig { + local version=$1 + : ${version:=""} + postgresTls='sslmode=disable' + case "$FABRIC_TLS" in + true) postgresTls='sslmode=require'; mysqlTls='?tls=custom' ;; + esac + + mkdir -p $FABRIC_CA_SERVER_HOME + echo "identity version: $identityLevel" + # Create base configuration using mysql + cat > $TESTCONFIG < /dev/null if [ $? != 0 ]; then diff --git a/scripts/fvt/fabric-ca_setup.sh b/scripts/fvt/fabric-ca_setup.sh index ec3fb42e0..b44e2f5bb 100755 --- a/scripts/fvt/fabric-ca_setup.sh +++ b/scripts/fvt/fabric-ca_setup.sh @@ -114,7 +114,7 @@ listFabricCa(){ function initFabricCa() { test -f $FABRIC_CA_SERVEREXEC || ErrorExit "fabric-ca executable not found in src tree" - $FABRIC_CA_SERVEREXEC init -c $RUNCONFIG $PARENTURL $args + $FABRIC_CA_SERVEREXEC init -c $RUNCONFIG $PARENTURL $args || return 1 echo "FABRIC_CA server initialized" if $($FABRIC_CA_DEBUG); then diff --git a/scripts/fvt/fabric-ca_utils b/scripts/fvt/fabric-ca_utils index 37ee6e95a..8672a77f6 100644 --- a/scripts/fvt/fabric-ca_utils +++ b/scripts/fvt/fabric-ca_utils @@ -906,3 +906,9 @@ function testStatus() { echo "$user_status_code $cert_status_code" } +function killserver { + echo "killing server $1" + kill -9 $1 + pollFabricCa "" "" "$CA_DEFAULT_PORT" stop 30 + return $? +} \ No newline at end of file diff --git a/scripts/regenDocs b/scripts/regenDocs index aee9cf8fe..0f11cdee2 100755 --- a/scripts/regenDocs +++ b/scripts/regenDocs @@ -82,7 +82,7 @@ echo -e "======================================\n" >> clientconfig.rst echo -e "::\n" >> clientconfig.rst # Sanitize the configuration files to remove any machine specific information and provide a generic config file -sed -e 's/cn:.*/cn: <<>>/' -e 's/pathlength:.*/pathlength: <<>>/' -e 's/abc/<<>>/' -e 's/pass:.*/pass: <<>>/' -e 's/'"$HOSTNAME"'/<<>>/' fabric-ca-server-config.yaml > server-config.yaml +sed -e 's/cn:.*/cn: <<>>/' -e 's/pathlength:.*/pathlength: <<>>/' -e 's/abc/<<>>/' -e 's/pass:.*/pass: <<>>/' -e 's/'"$HOSTNAME"'/<<>>/' -e 's/version:.*/version: <<>>/' fabric-ca-server-config.yaml > server-config.yaml sed -e 's/cn:.*/cn: <<>>/' -e 's/'"$HOSTNAME"'/<<>>/' -e 's/url:.*/url: <<>>/' fabric-ca-client-config.yaml > client-config.yaml # Insert a few spaces in front of all the lines in temp files created above (RST formatting purposes)