From 120b139936cf24fe8e3f300a40d3e96cf7e3889e Mon Sep 17 00:00:00 2001 From: Saad Karim Date: Wed, 5 Apr 2017 17:52:38 -0400 Subject: [PATCH] [FAB-3011] Fix max enrollment checking logic The max enrollment configuration setting is not treated correctly. The following values with associated semantics should be implemented. -1: Infinite number of enrollments Allow identities to set any value (including infinite). A value of 0 says to match the servers' value. 0: Enrollments disabled Both register and enroll requests return errors. >0: Number of enrollments allowed A user may not be registered with more than this many max enrollments. A one-time password is a max enrollments of 1. In order to adequately test this in different packages and make test code reusable, test code was moved to lib/test_util.go. Test code was changed to use these common utility methods. See [FAB-3011] for more info. Change-Id: I672177eb2839ab304395cccf38aae2dfdaa669fa Signed-off-by: Saad Karim Signed-off-by: Keith Smith --- api/client.go | 2 +- cmd/fabric-ca-client/config.go | 1 + cmd/fabric-ca-client/main_test.go | 37 ++- cmd/fabric-ca-server/config.go | 6 +- docs/source/users-guide.rst | 19 +- lib/ca.go | 8 +- lib/caconfig.go | 2 +- lib/client_test.go | 5 +- lib/client_whitebox_test.go | 10 +- lib/dasqlite_test.go | 13 +- lib/dbaccessor.go | 80 +++--- lib/ldap/client.go | 2 +- lib/server_test.go | 247 ++++++++++++++++-- lib/server_whitebox_test.go | 2 +- lib/serverauth.go | 8 +- lib/serverenroll.go | 16 +- lib/serverregister.go | 16 +- lib/spi/userregistry.go | 2 +- lib/test-util.go | 95 +++++++ lib/util.go | 51 +++- scripts/fvt/enrollments_test.sh | 6 +- .../ca2/fabric-ca-server-config.yaml | 4 +- .../rootca/ca1/fabric-ca-server-config.yaml | 5 +- 23 files changed, 490 insertions(+), 147 deletions(-) create mode 100644 lib/test-util.go diff --git a/api/client.go b/api/client.go index 27b2a67f9..af415b7e6 100644 --- a/api/client.go +++ b/api/client.go @@ -35,7 +35,7 @@ type RegistrationRequest struct { Secret string `json:"secret,omitempty" help:"The enrollment secret for the identity being registered"` // MaxEnrollments is the maximum number of times the secret can // be reused to enroll. - MaxEnrollments int `json:"max_enrollments,omitempty" help:"The maximum number of times the secret can be reused to enroll."` + MaxEnrollments int `json:"max_enrollments,omitempty" def:"-1" help:"The maximum number of times the secret can be reused to enroll."` // is returned in the response. // The identity's affiliation. // For example, an affiliation of "org1.department1" associates the identity with "department1" in "org1". diff --git a/cmd/fabric-ca-client/config.go b/cmd/fabric-ca-client/config.go index 0cb54cb14..f4bcee9ad 100644 --- a/cmd/fabric-ca-client/config.go +++ b/cmd/fabric-ca-client/config.go @@ -161,6 +161,7 @@ id: type: maxenrollments: affiliation: + maxenrollments: -1 attributes: - name: value: diff --git a/cmd/fabric-ca-client/main_test.go b/cmd/fabric-ca-client/main_test.go index 7d92a0c9e..455b7debe 100644 --- a/cmd/fabric-ca-client/main_test.go +++ b/cmd/fabric-ca-client/main_test.go @@ -105,6 +105,11 @@ const jsonConfig = `{ } }` +const ( + serverPort = 7054 + testdataDir = "homeDir" +) + var ( defYaml string fabricCADB = path.Join(tdDir, db) @@ -163,7 +168,7 @@ func TestCreateDefaultConfigFile(t *testing.T) { func TestClientCommandsNoTLS(t *testing.T) { os.Remove(fabricCADB) - srv = getServer() + srv = lib.TestGetServer(serverPort, testdataDir, "", -1, t) srv.HomeDir = tdDir srv.Config.Debug = true @@ -488,7 +493,6 @@ func testRevoke(t *testing.T) { err = RunMain([]string{cmdName, "revoke", "-u", "http://localhost:7054", "--revoke.name", "testRegister4", "--revoke.serial", "", "--revoke.aki", ""}) if err != nil { t.Errorf("User with root affiliation failed to revoke, error: %s", err) - } os.Remove(defYaml) // Delete default config file @@ -594,10 +598,10 @@ func TestGetCACert(t *testing.T) { func TestClientCommandsUsingConfigFile(t *testing.T) { os.Remove(fabricCADB) - srv = getServer() + srv = lib.TestGetServer(serverPort, testdataDir, "", -1, t) srv.Config.Debug = true - err := srv.RegisterBootstrapUser("admin", "adminpw", "bank1") + err := srv.RegisterBootstrapUser("admin", "adminpw", "org1") if err != nil { t.Errorf("Failed to register bootstrap user: %s", err) } @@ -626,15 +630,10 @@ func TestClientCommandsUsingConfigFile(t *testing.T) { func TestClientCommandsTLSEnvVar(t *testing.T) { os.Remove(fabricCADB) - srv = getServer() + srv = lib.TestGetServer(serverPort, testdataDir, "", -1, t) srv.Config.Debug = true - err := srv.RegisterBootstrapUser("admin", "adminpw", "bank1") - if err != nil { - t.Errorf("Failed to register bootstrap user: %s", err) - } - - err = srv.RegisterBootstrapUser("admin2", "adminpw2", "bank1") + err := srv.RegisterBootstrapUser("admin2", "adminpw2", "org1") if err != nil { t.Errorf("Failed to register bootstrap user: %s", err) } @@ -671,15 +670,10 @@ func TestClientCommandsTLSEnvVar(t *testing.T) { func TestClientCommandsTLS(t *testing.T) { os.Remove(fabricCADB) - srv = getServer() + srv = lib.TestGetServer(serverPort, testdataDir, "", -1, t) srv.Config.Debug = true - err := srv.RegisterBootstrapUser("admin", "adminpw", "bank1") - if err != nil { - t.Errorf("Failed to register bootstrap user: %s", err) - } - - err = srv.RegisterBootstrapUser("admin2", "adminpw2", "bank1") + err := srv.RegisterBootstrapUser("admin2", "adminpw2", "org1") if err != nil { t.Errorf("Failed to register bootstrap user: %s", err) } @@ -713,7 +707,7 @@ func TestClientCommandsTLS(t *testing.T) { func TestMultiCA(t *testing.T) { cleanMultiCADir() - srv = getServer() + srv = lib.TestGetServer(serverPort, testdataDir, "", -1, t) srv.HomeDir = "../../testdata" srv.Config.CAfiles = []string{"ca/rootca/ca1/fabric-ca-server-config.yaml", "ca/rootca/ca2/fabric-ca-server-config.yaml"} srv.CA.Config.CSR.Hosts = []string{"hostname"} @@ -746,7 +740,7 @@ func TestMultiCA(t *testing.T) { t.Errorf("client enroll -c -u failed: %s", err) } - err = RunMain([]string{cmdName, "register", "-c", testYaml, "-d", "--id.name", "testuser", "--id.type", "user", "--id.affiliation", "org1", "--caname", "rootca1"}) + err = RunMain([]string{cmdName, "register", "-c", testYaml, "-d", "--id.name", "testuser", "--id.type", "user", "--id.affiliation", "org2", "--caname", "rootca1"}) if err != nil { t.Errorf("client enroll -c -u failed: %s", err) } @@ -922,6 +916,9 @@ func startServer(home string, port int, t *testing.T) *lib.Server { CA: lib.CA{ Config: &lib.CAConfig{ Affiliations: affiliations, + Registry: lib.CAConfigRegistry{ + MaxEnrollments: -1, + }, }, }, } diff --git a/cmd/fabric-ca-server/config.go b/cmd/fabric-ca-server/config.go index 168e1744b..f4fd0828c 100644 --- a/cmd/fabric-ca-server/config.go +++ b/cmd/fabric-ca-server/config.go @@ -140,8 +140,8 @@ ca: ############################################################################# registry: # Maximum number of times a password/secret can be reused for enrollment - # (default: 0, which means there is no limit) - maxEnrollments: 0 + # (default: -1, which means there is no limit) + maxenrollments: -1 # Contains identity information which is used when LDAP is disabled identities: @@ -149,6 +149,7 @@ registry: pass: <<>> type: client affiliation: "" + maxenrollments: -1 attrs: hf.Registrar.Roles: "client,user,peer,validator,auditor,ca" hf.Registrar.DelegateRoles: "client,user,validator,auditor" @@ -374,7 +375,6 @@ func configInit() (err error) { } // Read the config - // viper.SetConfigFile(cfgFileName) viper.AutomaticEnv() // read in environment variables that match err = lib.UnmarshalConfig(serverCfg, viper.GetViper(), cfgFileName, true, true) if err != nil { diff --git a/docs/source/users-guide.rst b/docs/source/users-guide.rst index 80f40aded..22479d185 100644 --- a/docs/source/users-guide.rst +++ b/docs/source/users-guide.rst @@ -322,8 +322,8 @@ the server's home directory (see `Fabric CA Server <#server>`__ section more inf ############################################################################# registry: # Maximum number of times a password/secret can be reused for enrollment - # (default: 0, which means there is no limit) - maxEnrollments: 0 + # (default: -1, which means there is no limit) + maxenrollments: -1 # Contains identity information which is used when LDAP is disabled identities: @@ -810,12 +810,14 @@ To cause the Fabric CA server to listen on ``https`` rather than ``http``, set ``tls.enabled`` to ``true``. To limit the number of times that the same secret (or password) can be -used for enrollment, set the ``registry.maxEnrollments`` in the configuration +used for enrollment, set the ``registry.maxenrollments`` in the configuration file to the appropriate value. If you set the value to 1, the Fabric CA server allows passwords to only be used once for a particular enrollment -ID. If you set the value to 0, the Fabric CA server places no limit on +ID. If you set the value to -1, the Fabric CA server places no limit on the number of times that a secret can be reused for enrollment. The -default value is 0. +default value is -1. Setting the value to 0, the Fabric CA server will +disable enrollment for all identitiies and registeration of identities will +not be allowed. The Fabric CA server should now be listening on port 7054. @@ -1242,6 +1244,7 @@ file contains the following: name: type: user affiliation: org1.department1 + maxenrollments: -1 attributes: - name: hf.Revoker value: true @@ -1261,6 +1264,12 @@ and two attributes: "hf.Revoker" and "anotherAttrName". To register an identity with multiple attributes requires specifying all attribute names and values in the configuration file as shown above. +Setting `maxenrollments` to 0 or leaving it out from the configuration will result in the identity +being registerd to use the CA's max enrollment value. Furthermore, the max enrollment value for +an identity being registered cannot exceed the CA's max enrollment value. For example, if the CA's +max enrollment value is 5. Any new identity must have a value less than or equal to 5, and also +can't set it to -1 (infinite enrollments). + Next, let's register a peer identity which will be used to enroll the peer in the following section. The following command registers the **peer1** identity. Note that we choose to specify our own password (or secret) rather than letting the server generate one for us. diff --git a/lib/ca.go b/lib/ca.go index 802d10200..d1479eec6 100644 --- a/lib/ca.go +++ b/lib/ca.go @@ -187,7 +187,7 @@ func (ca *CA) initKeyMaterial(renew bool) error { log.Info("The CA key and certificate files already exist") log.Infof("Key file location: %s", keyFile) log.Infof("Certificate file location: %s", certFile) - err := ca.validateCert(certFile, keyFile) + err = ca.validateCert(certFile, keyFile) if err != nil { return fmt.Errorf("Validation of certificate and key failed: %s", err) } @@ -369,6 +369,7 @@ func (ca *CA) initConfig() (err error) { // Init config if not set if ca.Config == nil { ca.Config = new(CAConfig) + ca.Config.Registry.MaxEnrollments = -1 } // Set config defaults cfg := ca.Config @@ -615,17 +616,18 @@ func (ca *CA) addIdentity(id *CAConfigIdentity, errIfFound bool) error { return nil } - maxEnrollments, err := ca.getMaxEnrollments(id.MaxEnrollments) + id.MaxEnrollments, err = getMaxEnrollments(id.MaxEnrollments, ca.Config.Registry.MaxEnrollments) if err != nil { return err } + rec := spi.UserInfo{ Name: id.Name, Pass: id.Pass, Type: id.Type, Affiliation: id.Affiliation, Attributes: ca.convertAttrs(id.Attrs), - MaxEnrollments: maxEnrollments, + MaxEnrollments: id.MaxEnrollments, } err = ca.registry.InsertUser(rec) if err != nil { diff --git a/lib/caconfig.go b/lib/caconfig.go index 4559f0e5b..e9c7b4a54 100644 --- a/lib/caconfig.go +++ b/lib/caconfig.go @@ -99,7 +99,7 @@ type CAConfigDB struct { // CAConfigRegistry is the registry part of the server's config type CAConfigRegistry struct { - MaxEnrollments int `def:"0" help:"Maximum number of enrollments; valid if LDAP not enabled"` + MaxEnrollments int `def:"-1" help:"Maximum number of enrollments; valid if LDAP not enabled"` Identities []CAConfigIdentity } diff --git a/lib/client_test.go b/lib/client_test.go index b6c1ba5cb..a80eee100 100644 --- a/lib/client_test.go +++ b/lib/client_test.go @@ -48,8 +48,7 @@ const ( ) func TestClient(t *testing.T) { - - server := getServer(ctport1, path.Join(serversDir, "c1"), "", 1, t) + server := TestGetServer(ctport1, path.Join(serversDir, "c1"), "", 1, t) if server == nil { return } @@ -276,7 +275,7 @@ func testLoadBadCSRInfo(c *Client, t *testing.T) { func TestCustomizableMaxEnroll(t *testing.T) { os.Remove("../testdata/fabric-ca-server.db") - srv := getServer(ctport2, path.Join(serversDir, "c2"), "", 3, t) + srv := TestGetServer(ctport2, path.Join(serversDir, "c2"), "", 3, t) if srv == nil { return } diff --git a/lib/client_whitebox_test.go b/lib/client_whitebox_test.go index ae58242ae..49286f302 100644 --- a/lib/client_whitebox_test.go +++ b/lib/client_whitebox_test.go @@ -27,12 +27,10 @@ import ( ) const ( - whitePort = 7058 - rootDir = "rootDir" - testdataDir = "../testdata" - user = "admin" - pass = "adminpw" - serversDir = "testservers" + whitePort = 7058 + user = "admin" + pass = "adminpw" + serversDir = "testservers" ) var clientConfig = path.Join(testdataDir, "client-config.json") diff --git a/lib/dasqlite_test.go b/lib/dasqlite_test.go index fe2a0a9a7..c8ee2c982 100644 --- a/lib/dasqlite_test.go +++ b/lib/dasqlite_test.go @@ -157,10 +157,11 @@ func testUpdateUser(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{}, + MaxEnrollments: 1, } err := ta.Accessor.InsertUser(insert) @@ -180,9 +181,9 @@ func testUpdateUser(ta TestAccessor, t *testing.T) { t.Errorf("Error occured during querying of ID: %s, error: %s", insert.Name, err) } - err = user.Login(insert.Pass) + err = user.Login(insert.Pass, -1) if err != nil { - t.Error("Failed to update user's password") + t.Error("Failed to login in user: ", err) } } diff --git a/lib/dbaccessor.go b/lib/dbaccessor.go index b01d7b8e4..5a0d8aa93 100644 --- a/lib/dbaccessor.go +++ b/lib/dbaccessor.go @@ -340,59 +340,57 @@ func (u *DBUser) GetName() string { } // Login the user with a password -func (u *DBUser) Login(pass string) error { - log.Debugf("DB: Login identity %s with max enrollments of %d and state of %d", - u.Name, u.MaxEnrollments, u.State) +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) // Check the password if u.Pass != pass { return errors.New("Incorrect password") } + if u.MaxEnrollments == 0 { + return fmt.Errorf("Zero is an invalid value for maximum enrollments on identity '%s'", u.Name) + } + if u.State == -1 { return fmt.Errorf("User %s is revoked; access denied", u.Name) } - // If the maxEnrollments is set (i.e. >= 0), make sure we haven't exceeded - // this number of logins. The state variable keeps track of the number of - // previously successful logins. - if u.MaxEnrollments >= 0 { - // If maxEnrollments is set to 0, user has unlimited enrollment - if u.MaxEnrollments != 0 { - if u.State >= u.MaxEnrollments { - return fmt.Errorf("No more enrollments left. The maximum number of enrollments is %d", - u.MaxEnrollments) - } - } - - // Not exceeded, so attempt to increment the count - state := u.State + 1 - res, err := u.db.Exec(u.db.Rebind("UPDATE users SET state = ? WHERE (id = ?)"), state, u.Name) - if err != nil { - return fmt.Errorf("Failed to update state of identity %s to %d: %s", - u.Name, state, err) - } - - numRowsAffected, err := res.RowsAffected() - - if err != nil { - return fmt.Errorf("db.RowsAffected failed: %s", err) - } - - if numRowsAffected == 0 { - return fmt.Errorf("no rows were affected when updating the state of identity %s", - u.Name) - } - - if numRowsAffected != 1 { - return fmt.Errorf("%d rows were affected when updating the state of identity %s", - numRowsAffected, u.Name) - } - - log.Debugf("DB: Successfully incremented state for identity %s to %d", - u.Name, state) + // If max enrollment value of user is greater than allowed by CA, using CA max enrollment value for user + if u.MaxEnrollments > caMaxEnrollments || (u.MaxEnrollments == -1 && caMaxEnrollments != -1) { + log.Debugf("Max enrollment value (%d) of identity is greater than allowed by CA, using CA max enrollment value of %d", u.MaxEnrollments, caMaxEnrollments) + u.MaxEnrollments = caMaxEnrollments + } + + // If maxEnrollments is set to -1, user has unlimited enrollment + // If the maxEnrollments is set (i.e. >= 1), make sure we haven't exceeded this number of logins. + // The state variable keeps track of the number of previously successful logins. + if u.MaxEnrollments != -1 && u.State >= u.MaxEnrollments { + return fmt.Errorf("The identity %s has already enrolled %d times, it has reached its maximum enrollment allowance", u.Name, u.MaxEnrollments) + } + + // Not exceeded, so attempt to increment the count + state := u.State + 1 + res, err := u.db.Exec(u.db.Rebind("UPDATE users SET state = ? WHERE (id = ?)"), state, u.Name) + if err != nil { + return fmt.Errorf("Failed to update state of identity %s to %d: %s", u.Name, state, err) + } + + numRowsAffected, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("db.RowsAffected failed: %s", err) + } + + if numRowsAffected == 0 { + return fmt.Errorf("No rows were affected when updating the state of identity %s", u.Name) } + if numRowsAffected != 1 { + return fmt.Errorf("%d rows were affected when updating the state of identity %s", numRowsAffected, u.Name) + } + + log.Debugf("Successfully incremented state for identity %s to %d", u.Name, state) + log.Debugf("DB: identity %s successfully logged in", u.Name) return nil diff --git a/lib/ldap/client.go b/lib/ldap/client.go index eb265432f..6909708b9 100644 --- a/lib/ldap/client.go +++ b/lib/ldap/client.go @@ -278,7 +278,7 @@ func (u *User) GetName() string { } // Login logs a user in using password -func (u *User) Login(password string) error { +func (u *User) Login(password string, caMaxEnrollment int) error { // Get a connection to use to bind over as the user to check the password conn, err := u.client.newConnection() diff --git a/lib/server_test.go b/lib/server_test.go index 9ed63836d..e320ec08e 100644 --- a/lib/server_test.go +++ b/lib/server_test.go @@ -47,7 +47,7 @@ const ( ) func TestServerInit(t *testing.T) { - server := getRootServer(t) + server := TestGetRootServer(t) if server == nil { return } @@ -76,7 +76,7 @@ func TestRootServer(t *testing.T) { var recs []CertRecord // Start the server - server := getRootServer(t) + server := TestGetRootServer(t) if server == nil { return } @@ -175,7 +175,7 @@ func TestProfiling(t *testing.T) { // Start the server with profiling disabled os.Setenv(pportEnvVar, strconv.Itoa(-1)) - server := getServer(rootPort, rootDir, "", 0, t) + server := TestGetServer(rootPort, rootDir, "", -1, t) if server == nil { return } @@ -195,7 +195,7 @@ func TestProfiling(t *testing.T) { // Start the server with profiling enabled but port set to server port os.Setenv(pportEnvVar, strconv.Itoa(rootPort)) - server = getServer(rootPort, rootDir, "", 0, t) + server = TestGetServer(rootPort, rootDir, "", -1, t) if server == nil { return } @@ -207,7 +207,7 @@ func TestProfiling(t *testing.T) { // Start the server with profiling enabled os.Setenv(pportEnvVar, strconv.Itoa(pport)) defer os.Unsetenv(pportEnvVar) - server = getServer(rootPort, rootDir, "", 0, t) + server = TestGetServer(rootPort, rootDir, "", -1, t) if server == nil { return } @@ -245,7 +245,7 @@ func TestIntermediateServer(t *testing.T) { var err error // Start the root server - rootServer := getRootServer(t) + rootServer := TestGetRootServer(t) if rootServer == nil { return } @@ -268,7 +268,7 @@ func TestIntermediateServer(t *testing.T) { func TestIntermediateServerWithTLS(t *testing.T) { var err error - rootServer := getRootServer(t) + rootServer := TestGetRootServer(t) if rootServer == nil { return } @@ -289,7 +289,7 @@ func TestIntermediateServerWithTLS(t *testing.T) { }() parentURL := fmt.Sprintf("https://admin:adminpw@localhost:%d", rootPort) - intermediateServer := getServer(intermediatePort, intermediateDir, parentURL, 0, t) + intermediateServer := TestGetServer(intermediatePort, intermediateDir, parentURL, -1, t) if intermediateServer == nil { return } @@ -333,7 +333,7 @@ func TestIntermediateServerWithTLS(t *testing.T) { } func TestRunningTLSServer(t *testing.T) { - srv := getServer(rootPort, testdataDir, "", 0, t) + srv := TestGetServer(rootPort, testdataDir, "", -1, t) srv.Config.TLS.Enabled = true srv.Config.TLS.CertFile = "../testdata/tls_server-cert.pem" @@ -371,7 +371,7 @@ func TestRunningTLSServer(t *testing.T) { func TestDefaultDatabase(t *testing.T) { TestEnd(t) - srv := getServer(rootPort, testdataDir, "", 0, t) + srv := TestGetServer(rootPort, testdataDir, "", -1, t) err := srv.Start() if err != nil { @@ -391,7 +391,7 @@ func TestDefaultDatabase(t *testing.T) { func TestBadAuthHeader(t *testing.T) { // Start the server - server := getRootServer(t) + server := TestGetRootServer(t) if server == nil { return } @@ -472,7 +472,7 @@ func TestTLSAuthClient(t *testing.T) { func TestMultiCAConfigs(t *testing.T) { t.Log("TestMultiCA...") - srv := getServer(rootPort, testdataDir, "", 0, t) + srv := TestGetServer(rootPort, testdataDir, "", -1, t) srv.Config.CAfiles = []string{"ca/ca1/fabric-ca-server-config.yaml", "ca/ca1/fabric-ca-server-config.yaml", "ca/ca2/fabric-ca-server-config.yaml"} srv.CA.Config.CSR.Hosts = []string{"hostname"} t.Logf("Server configuration: %+v\n", srv.Config) @@ -579,9 +579,8 @@ func TestMultiCAConfigs(t *testing.T) { } func TestDefaultCAWithSetCAName(t *testing.T) { - srv := getServer(rootPort, testdataDir, "", 0, t) + srv := getServer(rootPort, testdataDir, "", -1, t) srv.CA.Config.CA.Name = "DefaultCA" - t.Logf("Server configuration: %+v\n", srv.Config) // Starting server with two cas with same name err := srv.Start() @@ -606,7 +605,7 @@ func TestDefaultCAWithSetCAName(t *testing.T) { } func TestMultiCAWithIntermediate(t *testing.T) { - srv := getServer(rootPort, testdataDir, "", 0, t) + srv := TestGetServer(rootPort, testdataDir, "", -1, t) srv.Config.CAfiles = []string{"ca/rootca/ca1/fabric-ca-server-config.yaml", "ca/rootca/ca2/fabric-ca-server-config.yaml"} srv.CA.Config.CSR.Hosts = []string{"hostname"} t.Logf("Server configuration: %+v\n", srv.Config) @@ -614,10 +613,10 @@ func TestMultiCAWithIntermediate(t *testing.T) { // Starting server with two cas with same name err := srv.Start() if err != nil { - t.Fatal("Failed to start server: ", err) + t.Fatalf("Failed to start server: %s", err) } - intermediatesrv := getServer(intermediatePort, testdataDir, "", 0, t) + intermediatesrv := TestGetServer(intermediatePort, testdataDir, "", -1, t) intermediatesrv.Config.CAfiles = []string{"ca/intermediateca/ca1/fabric-ca-server-config.yaml", "ca/intermediateca/ca2/fabric-ca-server-config.yaml"} intermediatesrv.CA.Config.CSR.Hosts = []string{"hostname"} @@ -650,7 +649,7 @@ func TestMultiCAWithIntermediate(t *testing.T) { func TestDefaultMultiCA(t *testing.T) { t.Log("TestDefaultMultiCA...") - srv := getServer(rootPort, "multica", "", -1, t) + srv := TestGetServer(rootPort, "multica", "", -1, t) srv.Config.CAcount = 4 // Starting 4 default CA instances srv.Config.CAfiles = []string{"fabric-ca1-config.yaml"} @@ -683,9 +682,201 @@ func TestDefaultMultiCA(t *testing.T) { } } +func TestMaxEnrollmentInfinite(t *testing.T) { + os.RemoveAll(rootDir) + t.Log("Test max enrollment infinite") + // Starting server/ca with infinite enrollments + srv := TestGetServer(rootPort, rootDir, "", -1, t) + err := srv.Start() + if err != nil { + t.Fatalf("Server start failed: %s", err) + } + client := getRootClient() + id, err := client.Enroll(&api.EnrollmentRequest{ + Name: "admin", + Secret: "adminpw", + }) + if err != nil { + t.Error("Enrollment failed, error: ", err) + } + id.Identity.Store() + // Names of users are of the form: + // me__ + // where "me" stands for "max enrollments" + + // Registering user with missing max enrollment value + _, err = id.Identity.Register(&api.RegistrationRequest{ + Name: "me_missing_-1", + Type: "client", + Affiliation: "org2", + }) + if err != nil { + t.Errorf("Failed to register me_missing_-1, error: %s", err) + } + + // Registering user with infinite max enrollments (-1) + _, err = id.Identity.Register(&api.RegistrationRequest{ + Name: "me_-1_-1", + Type: "client", + Affiliation: "org2", + MaxEnrollments: -1, + }) + if err != nil { + t.Errorf("Failed to register me_-1_-1, error: %s", err) + } + + // Registering user with zero max enrollments, will take value of CA's max enrollment + _, err = id.Identity.Register(&api.RegistrationRequest{ + Name: "me_0_-1", + Type: "client", + Affiliation: "org2", + MaxEnrollments: 0, + }) + if err != nil { + t.Errorf("Failed to register me_0_-1, error: %s", err) + } + + // Registering user with 1000 max enrollments + _, err = id.Identity.Register(&api.RegistrationRequest{ + Name: "me_1000_-1", + Type: "client", + Affiliation: "org2", + MaxEnrollments: 1000, + }) + if err != nil { + t.Errorf("Failed to register me_1000_-1, error: %s", err) + } + err = srv.Stop() + if err != nil { + t.Errorf("Server stop failed: %s", err) + } + os.RemoveAll(rootDir) +} + +func TestMaxEnrollmentDisabled(t *testing.T) { + os.RemoveAll(rootDir) + t.Log("Test max enrollment disabled") + // Starting server/ca with infinite enrollments + srv := TestGetServer(rootPort, rootDir, "", -1, t) + err := srv.Start() + if err != nil { + t.Fatalf("Server start failed: %s", err) + } + client := getRootClient() + id, err := client.Enroll(&api.EnrollmentRequest{ + Name: "admin", + Secret: "adminpw", + }) + if err != nil { + t.Errorf("Enrollment failed: %s", err) + } + // Disable enrollment + srv.CA.Config.Registry.MaxEnrollments = 0 + // Make sure both registration and enrollment fail + _, err = id.Identity.Register(&api.RegistrationRequest{ + Name: "me_0_0", + Type: "client", + Affiliation: "org2", + }) + if err == nil { + t.Error("Registration should have failed but didn't") + } + _, err = client.Enroll(&api.EnrollmentRequest{ + Name: "admin", + Secret: "adminpw", + }) + if err == nil { + t.Error("Enrollment should have failed but didn't") + } + err = srv.Stop() + if err != nil { + t.Errorf("Server stop failed: %s", err) + } + os.RemoveAll(rootDir) +} + +func TestMaxEnrollmentLimited(t *testing.T) { + os.RemoveAll(rootDir) + t.Log("Test max enrollment limited") + // Starting server/ca with max enrollments of 1 + srv := TestGetServer(rootPort, rootDir, "", 1, t) + err := srv.Start() + if err != nil { + t.Fatalf("Server start failed: %s", err) + } + client := getRootClient() + id, err := client.Enroll(&api.EnrollmentRequest{ + Name: "admin", + Secret: "adminpw", + }) + if err != nil { + t.Error("Enrollment failed, error: ", err) + } + id.Identity.Store() + _, err = client.Enroll(&api.EnrollmentRequest{ + Name: "admin", + Secret: "adminpw", + }) + if err == nil { + t.Error("Enrollments should have been limited to 1 but allowed 2") + } + // Registering user with missing max enrollment value + // Names of users are of the form: + // me__ + // where "me" stands for "max enrollments" + _, err = id.Identity.Register(&api.RegistrationRequest{ + Name: "me_-1_1", + Type: "client", + Affiliation: "org2", + MaxEnrollments: -1, + }) + if err == nil { + t.Error("Should have failed to register infinite but didn't") + } + _, err = id.Identity.Register(&api.RegistrationRequest{ + Name: "me_0_1", + Type: "client", + Affiliation: "org2", + MaxEnrollments: 0, + }) + if err != nil { + t.Errorf("Failed to register me_0_1, error: %s", err) + } + user, err := srv.CA.DBAccessor().GetUserInfo("me_0_1") + if err != nil { + t.Errorf("Failed to find user 'me_0_1,' in database") + } + if user.MaxEnrollments != 1 { + t.Error("Failed to correctly set max enrollment value for a user registering with max enrollment of 0") + } + _, err = id.Identity.Register(&api.RegistrationRequest{ + Name: "me_1_1", + Type: "client", + Affiliation: "org2", + MaxEnrollments: 1, + }) + if err != nil { + t.Errorf("Failed to register me_1_1, error: %s", err) + } + _, err = id.Identity.Register(&api.RegistrationRequest{ + Name: "me_2_1", + Type: "client", + Affiliation: "org2", + MaxEnrollments: 2, + }) + if err == nil { + t.Error("Should have failed to register me_2_1 but didn't") + } + err = srv.Stop() + if err != nil { + t.Errorf("Server stop failed: %s", err) + } + os.RemoveAll(rootDir) +} + // Configure server to start server with no client authentication required func testNoClientCert(t *testing.T) { - srv := getServer(rootPort, testdataDir, "", 0, t) + srv := TestGetServer(rootPort, testdataDir, "", -1, t) srv = getTLSConfig(srv, "NoClientCert", []string{}) err := srv.Start() @@ -716,7 +907,7 @@ func testNoClientCert(t *testing.T) { // Configure server to start with no client authentication required // Root2.pem does not exists, server should still start because no client auth is requred func testInvalidRootCertWithNoClientAuth(t *testing.T) { - srv := getServer(rootPort, testdataDir, "", 0, t) + srv := TestGetServer(rootPort, testdataDir, "", -1, t) srv = getTLSConfig(srv, "NoClientCert", []string{"../testdata/root.pem", "../testdata/root2.pem"}) err := srv.Start() @@ -733,7 +924,7 @@ func testInvalidRootCertWithNoClientAuth(t *testing.T) { // Configure server to start with client authentication required // Root2.pem does not exists, server should fail to start func testInvalidRootCertWithClientAuth(t *testing.T) { - srv := getServer(rootPort, testdataDir, "", 0, t) + srv := TestGetServer(rootPort, testdataDir, "", -1, t) srv = getTLSConfig(srv, "RequireAndVerifyClientCert", []string{"../testdata/root.pem", "../testdata/root2.pem"}) err := srv.Start() @@ -744,7 +935,7 @@ func testInvalidRootCertWithClientAuth(t *testing.T) { // Configure server to start with client authentication required func testClientAuth(t *testing.T) { - srv := getServer(rootPort, testdataDir, "", 0, t) + srv := TestGetServer(rootPort, testdataDir, "", -1, t) srv = getTLSConfig(srv, "RequireAndVerifyClientCert", []string{"../testdata/root.pem"}) err := srv.Start() @@ -792,7 +983,7 @@ func testClientAuth(t *testing.T) { func testIntermediateServer(idx int, t *testing.T) { // Init the intermediate server - intermediateServer := getIntermediateServer(idx, t) + intermediateServer := TestGetIntermediateServer(idx, t) if intermediateServer == nil { return } @@ -819,7 +1010,7 @@ func testIntermediateServer(idx int, t *testing.T) { // This test assumes that sqlite is the database used in the tests func TestSqliteLocking(t *testing.T) { // Start the server - server := getServer(rootPort, rootDir, "", 0, t) + server := TestGetServer(rootPort, rootDir, "", -1, t) if server == nil { return } @@ -870,6 +1061,7 @@ func TestEnd(t *testing.T) { os.Remove("../testdata/ca-cert.pem") os.Remove("../testdata/ca-key.pem") os.Remove("../testdata/fabric-ca-server.db") + os.RemoveAll("../testdata/msp/") os.RemoveAll(rootDir) os.RemoveAll(intermediateDir) os.RemoveAll("multica") @@ -893,7 +1085,8 @@ func cleanMultiCADir() { os.RemoveAll(filepath.Join(path, "msp")) } } - + os.RemoveAll("../testdata/ca/intermediateca/ca1/msp") + os.RemoveAll("multica") } func getRootServerURL() string { @@ -901,7 +1094,7 @@ func getRootServerURL() string { } func getRootServer(t *testing.T) *Server { - return getServer(rootPort, rootDir, "", 0, t) + return getServer(rootPort, rootDir, "", -1, t) } func getIntermediateServer(idx int, t *testing.T) *Server { @@ -909,7 +1102,7 @@ func getIntermediateServer(idx int, t *testing.T) *Server { intermediatePort, path.Join(intermediateDir, strconv.Itoa(idx)), getRootServerURL(), - 0, + -1, t) } diff --git a/lib/server_whitebox_test.go b/lib/server_whitebox_test.go index ff46b0946..2c12a73e4 100644 --- a/lib/server_whitebox_test.go +++ b/lib/server_whitebox_test.go @@ -34,7 +34,7 @@ const ( func TestGetAffliation(t *testing.T) { // Start the server at an available port (using port 0 will make OS to // pick an available port) - srv := getServer(serverPort, testdataDir, "", 0, t) + srv := getServer(serverPort, testdataDir, "", -1, t) err := srv.Start() if err != nil { diff --git a/lib/serverauth.go b/lib/serverauth.go index 93afe7564..4c329c517 100644 --- a/lib/serverauth.go +++ b/lib/serverauth.go @@ -125,7 +125,13 @@ func (ah *fcaAuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) erro log.Debugf("Failed to get identity '%s': %s", user, err) return authError } - err = u.Login(pwd) + caMaxEnrollments := ah.server.caMap[req.CAName].Config.Registry.MaxEnrollments + if caMaxEnrollments == 0 { + msg := fmt.Sprintf("Enrollments are disabled; user '%s' cannot enroll", user) + log.Debugf(msg) + return errors.New(msg) + } + err = u.Login(pwd, caMaxEnrollments) if err != nil { log.Debugf("Failed to login '%s': %s", user, err) return authError diff --git a/lib/serverenroll.go b/lib/serverenroll.go index d9b5f4398..d98da1ccd 100644 --- a/lib/serverenroll.go +++ b/lib/serverenroll.go @@ -77,8 +77,15 @@ func newSignHandler(server *Server, endpoint string) (h http.Handler, err error) // Authentication has already occurred for both enroll and reenroll prior // to calling this function in auth.go. func (sh *signHandler) Handle(w http.ResponseWriter, r *http.Request) error { - log.Debugf("Received request for endpoint %s", sh.endpoint) + err := sh.handle(w, r) + if err != nil { + log.Errorf("Enrollment failure: %s", err) + } + return err +} + +func (sh *signHandler) handle(w http.ResponseWriter, r *http.Request) error { // Read the request's body body, err := ioutil.ReadAll(r.Body) @@ -97,6 +104,9 @@ func (sh *signHandler) Handle(w http.ResponseWriter, r *http.Request) error { log.Debugf("Enrollment request: %+v\n", req) caname := r.Header.Get(caHdrName) + if sh.server.caMap[caname].Config.Registry.MaxEnrollments == 0 { + return errors.New("The enroll API is disabled") + } // Make any authorization checks needed, depending on the contents // of the CSR (Certificate Signing Request) @@ -108,9 +118,7 @@ func (sh *signHandler) Handle(w http.ResponseWriter, r *http.Request) error { // Sign the certificate cert, err := sh.server.caMap[caname].enrollSigner.Sign(req.SignRequest) if err != nil { - err = fmt.Errorf("Failed signing for endpoint %s: %s", sh.endpoint, err) - log.Error(err.Error()) - return err + return fmt.Errorf("Failed signing: %s", err) } // Send the response with the cert and the server info diff --git a/lib/serverregister.go b/lib/serverregister.go index f4db4e431..3e35b99bd 100644 --- a/lib/serverregister.go +++ b/lib/serverregister.go @@ -129,25 +129,23 @@ func (h *registerHandler) validateID(req *api.RegistrationRequestNet, caname str // registerUserID registers a new user and its enrollmentID, role and state func (h *registerHandler) registerUserID(req *api.RegistrationRequestNet, caname string) (string, error) { log.Debugf("Registering user id: %s\n", req.Name) + var err error if req.Secret == "" { req.Secret = util.RandomString(12) } - maxEnrollments := h.server.caMap[caname].Config.Registry.MaxEnrollments - - if (req.MaxEnrollments > maxEnrollments && maxEnrollments != 0) || (req.MaxEnrollments < 0) { - return "", fmt.Errorf("Invalid max enrollment value specified, value must be equal to or less than %d", maxEnrollments) - } + caMaxEnrollments := h.server.caMap[caname].Config.Registry.MaxEnrollments - if req.MaxEnrollments == 0 && maxEnrollments != 0 { - return "", fmt.Errorf("Unlimited enrollments not allowed, value must be equal to or less than %d", maxEnrollments) + req.MaxEnrollments, err = getMaxEnrollments(req.MaxEnrollments, caMaxEnrollments) + if err != nil { + return "", err } // Make sure delegateRoles is not larger than roles roles := GetAttrValue(req.Attributes, attrRoles) delegateRoles := GetAttrValue(req.Attributes, attrDelegateRoles) - err := util.IsSubsetOf(delegateRoles, roles) + err = util.IsSubsetOf(delegateRoles, roles) if err != nil { return "", fmt.Errorf("delegateRoles is superset of roles: %s", err) } @@ -158,7 +156,7 @@ func (h *registerHandler) registerUserID(req *api.RegistrationRequestNet, caname Type: req.Type, Affiliation: req.Affiliation, Attributes: req.Attributes, - MaxEnrollments: maxEnrollments, + MaxEnrollments: req.MaxEnrollments, } registry := h.server.caMap[caname].registry diff --git a/lib/spi/userregistry.go b/lib/spi/userregistry.go index 379c29154..8e2588c7e 100644 --- a/lib/spi/userregistry.go +++ b/lib/spi/userregistry.go @@ -38,7 +38,7 @@ type User interface { // Returns the enrollment ID of the user GetName() string // Login the user with a password - Login(password string) error + Login(password string, caMaxEnrollment int) error // Get the complete path for the user's affiliation. GetAffiliationPath() []string // GetAttribute returns the value for an attribute name diff --git a/lib/test-util.go b/lib/test-util.go new file mode 100644 index 000000000..49221b707 --- /dev/null +++ b/lib/test-util.go @@ -0,0 +1,95 @@ +/* +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 lib + +import ( + "fmt" + "os" + "path" + "strconv" + "testing" +) + +const ( + rootPort = 7075 + rootDir = "rootDir" + intermediatePort = 7076 + intermediateDir = "intDir" + testdataDir = "../testdata" +) + +func getRootServerURL() string { + return fmt.Sprintf("http://admin:adminpw@localhost:%d", rootPort) +} + +// TestGetRootServer creates a server with root configuration +func TestGetRootServer(t *testing.T) *Server { + return TestGetServer(rootPort, rootDir, "", -1, t) +} + +// TestGetIntermediateServer creates a server with intermediate server configuration +func TestGetIntermediateServer(idx int, t *testing.T) *Server { + return TestGetServer( + intermediatePort, + path.Join(intermediateDir, strconv.Itoa(idx)), + getRootServerURL(), + -1, + t) +} + +// TestGetServer creates and returns a pointer to a server struct +func TestGetServer(port int, home, parentURL string, maxEnroll int, t *testing.T) *Server { + if home != testdataDir { + os.RemoveAll(home) + } + affiliations := map[string]interface{}{ + "hyperledger": map[string]interface{}{ + "fabric": []string{"ledger", "orderer", "security"}, + "fabric-ca": nil, + "sdk": nil, + }, + "org2": nil, + } + srv := &Server{ + Config: &ServerConfig{ + Port: port, + Debug: true, + }, + CA: CA{ + Config: &CAConfig{ + Intermediate: IntermediateCA{ + ParentServer: ParentServer{ + URL: parentURL, + }, + }, + Affiliations: affiliations, + Registry: CAConfigRegistry{ + MaxEnrollments: maxEnroll, + }, + }, + }, + HomeDir: home, + } + // The bootstrap user's affiliation is the empty string, which + // means the user is at the affiliation root + err := srv.RegisterBootstrapUser("admin", "adminpw", "") + if err != nil { + t.Errorf("Failed to register bootstrap user: %s", err) + return nil + } + return srv +} diff --git a/lib/util.go b/lib/util.go index 921c13c79..b0871c7f2 100644 --- a/lib/util.go +++ b/lib/util.go @@ -85,9 +85,10 @@ func LoadPEMCertPool(certFiles []string) (*x509.CertPool, error) { return certPool, nil } -// UnmarshalConfig ... -func UnmarshalConfig(config interface{}, vp *viper.Viper, caFile string, server, viperIssue327WorkAround bool) error { - vp.SetConfigFile(caFile) +// UnmarshalConfig will use the viperunmarshal workaround to unmarshal a +// configuration file into a struct +func UnmarshalConfig(config interface{}, vp *viper.Viper, configFile string, server, viperIssue327WorkAround bool) error { + vp.SetConfigFile(configFile) err := vp.ReadInConfig() if err != nil { return fmt.Errorf("Failed to read config file: %s", err) @@ -107,25 +108,25 @@ func UnmarshalConfig(config interface{}, vp *viper.Viper, caFile string, server, } err = util.ViperUnmarshal(config, sliceFields, vp) if err != nil { - return fmt.Errorf("Incorrect format in file '%s': %s", caFile, err) + return fmt.Errorf("Incorrect format in file '%s': %s", configFile, err) } if server { serverCfg := config.(*ServerConfig) err = util.ViperUnmarshal(&serverCfg.CAcfg, sliceFields, vp) if err != nil { - return fmt.Errorf("Incorrect format in file '%s': %s", caFile, err) + return fmt.Errorf("Incorrect format in file '%s': %s", configFile, err) } } } else { err = vp.Unmarshal(config) if err != nil { - return fmt.Errorf("Incorrect format in file '%s': %s", caFile, err) + return fmt.Errorf("Incorrect format in file '%s': %s", configFile, err) } if server { serverCfg := config.(*ServerConfig) err = vp.Unmarshal(&serverCfg.CAcfg) if err != nil { - return fmt.Errorf("Incorrect format in file '%s': %s", caFile, err) + return fmt.Errorf("Incorrect format in file '%s': %s", configFile, err) } } } @@ -142,3 +143,39 @@ func GetAttrValue(attrs []api.Attribute, name string) string { } return "" } + +func getMaxEnrollments(userMaxEnrollments int, caMaxEnrollments int) (int, error) { + log.Debugf("Max enrollment value verification - User specified max enrollment: %d, CA max enrollment: %d", userMaxEnrollments, caMaxEnrollments) + if userMaxEnrollments < -1 { + return 0, fmt.Errorf("Max enrollment in registration request may not be less than -1, but was %d", userMaxEnrollments) + } + switch caMaxEnrollments { + case -1: + if userMaxEnrollments == 0 { + // The user is requesting the matching limit of the CA, so gets infinite + return caMaxEnrollments, nil + } + // There is no CA max enrollment limit, so simply use the user requested value + return userMaxEnrollments, nil + case 0: + // The CA max enrollment is 0, so registration is disabled. + return 0, errors.New("Registration is disabled") + default: + switch userMaxEnrollments { + case -1: + // User requested infinite enrollments is not allowed + return 0, errors.New("Registration for infinite enrollments is not allowed") + case 0: + // User is requesting the current CA maximum + return caMaxEnrollments, nil + default: + // User is requesting a specific positive value; make sure it doesn't exceed the CA maximum. + if userMaxEnrollments > caMaxEnrollments { + return 0, fmt.Errorf("Requested enrollments (%d) exceeds maximum allowable enrollments (%d)", + userMaxEnrollments, caMaxEnrollments) + } + // otherwise, use the requested maximum + return userMaxEnrollments, nil + } + } +} diff --git a/scripts/fvt/enrollments_test.sh b/scripts/fvt/enrollments_test.sh index b0429db8e..5a5cb779f 100755 --- a/scripts/fvt/enrollments_test.sh +++ b/scripts/fvt/enrollments_test.sh @@ -205,9 +205,9 @@ trap "CleanUp 1; exit 1" INT test "$currId" != "$prevId" && ErrorMsg "Prior and current certificates are different" prevId="$currId" -# explicitly set value to '0' +# explicitly set value to '-1' # user enrollment unlimited - MAX_ENROLL=0 + MAX_ENROLL=-1 $SCRIPTDIR/fabric-ca_setup.sh -R -x $CA_CFG_PATH $SCRIPTDIR/fabric-ca_setup.sh -I -S -X -m $MAX_ENROLL i=0 @@ -219,7 +219,7 @@ trap "CleanUp 1; exit 1" INT prevId="$currId" done -# implicitly set value to '0' (default) +# implicitly set value to '-1' (default) # user enrollment unlimited $SCRIPTDIR/fabric-ca_setup.sh -R -x $CA_CFG_PATH test -d $CA_CFG_PATH || mkdir $CA_CFG_PATH diff --git a/testdata/ca/intermediateca/ca2/fabric-ca-server-config.yaml b/testdata/ca/intermediateca/ca2/fabric-ca-server-config.yaml index 05b0d6cc7..a50f484f0 100644 --- a/testdata/ca/intermediateca/ca2/fabric-ca-server-config.yaml +++ b/testdata/ca/intermediateca/ca2/fabric-ca-server-config.yaml @@ -36,8 +36,8 @@ ca: ############################################################################# registry: # Maximum number of times a password/secret can be reused for enrollment - # (default: 0, which means there is no limit) - maxEnrollments: 0 + # (default: -1, which means there is no limit) + maxEnrollments: -1 # Contains user information which is used when LDAP is disabled identities: diff --git a/testdata/ca/rootca/ca1/fabric-ca-server-config.yaml b/testdata/ca/rootca/ca1/fabric-ca-server-config.yaml index 8cb471c17..df1238cca 100644 --- a/testdata/ca/rootca/ca1/fabric-ca-server-config.yaml +++ b/testdata/ca/rootca/ca1/fabric-ca-server-config.yaml @@ -36,8 +36,8 @@ csr: ############################################################################# registry: # Maximum number of times a password/secret can be reused for enrollment - # (default: 0, which means there is no limit) - maxEnrollments: 0 + # (default: -1, which means there is no limit) + maxEnrollments: -1 # Contains user information which is used when LDAP is disabled identities: @@ -45,6 +45,7 @@ registry: pass: adminca1pw type: client affiliation: "" + maxenrollments: -1 attrs: hf.Registrar.Roles: "client,user,peer,validator,auditor,ca" hf.Registrar.DelegateRoles: "client,user,validator,auditor"