From df741bc72039791fb654930e3bad7df52a8c023c Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Fri, 2 Sep 2016 10:32:27 -0400 Subject: [PATCH] Add support for dynamically registering a user with attributes Signed-off-by: Keith Smith Change-Id: I2d8883fbc19d99bb80815ec764d115af06e1be96 --- core/chaincode/exectransaction_test.go | 2 +- core/crypto/crypto_test.go | 2 +- docs/Setup/NodeSDK-setup.md | 7 +- .../go/asset_management/asset_management.go | 2 +- .../asset_management02_test.go | 2 +- .../asset_management_with_roles.go | 4 +- .../asset_management_with_roles_test.go | 2 +- .../go/rbac_tcerts_no_attrs/rbac_test.go | 2 +- membersrvc/ca/aca.go | 8 +- membersrvc/ca/ca.go | 80 +++++- membersrvc/ca/eca.go | 7 +- membersrvc/ca/ecaa.go | 4 +- membersrvc/ca/membersrvc_test.go | 2 +- membersrvc/ca/tca_test.go | 9 +- membersrvc/ca/tlsca_test.go | 2 +- membersrvc/protos/ca.pb.go | 30 ++- membersrvc/protos/ca.proto | 16 +- membersrvc/server.go | 2 +- sdk/node/bin/run-unit-tests.sh | 29 ++- sdk/node/src/hfc.ts | 28 ++- .../unit/asset-mgmt-with-dynamic-roles.js | 230 ++++++++++++++++++ sdk/node/test/unit/registrar.js | 125 +++++++--- 22 files changed, 508 insertions(+), 87 deletions(-) create mode 100644 sdk/node/test/unit/asset-mgmt-with-dynamic-roles.js diff --git a/core/chaincode/exectransaction_test.go b/core/chaincode/exectransaction_test.go index 6d9624345de..e35daf963f9 100644 --- a/core/chaincode/exectransaction_test.go +++ b/core/chaincode/exectransaction_test.go @@ -61,7 +61,7 @@ func initMemSrvc() (net.Listener, error) { ca.CacheConfiguration() // Cache configuration aca := ca.NewACA() - eca := ca.NewECA() + eca := ca.NewECA(aca) tca := ca.NewTCA(eca) tlsca := ca.NewTLSCA(eca) diff --git a/core/crypto/crypto_test.go b/core/crypto/crypto_test.go index ec2efe6eb1d..0584ad17fa0 100644 --- a/core/crypto/crypto_test.go +++ b/core/crypto/crypto_test.go @@ -1543,7 +1543,7 @@ func setup() { func initPKI() { ca.CacheConfiguration() // Need cache the configuration first aca = ca.NewACA() - eca = ca.NewECA() + eca = ca.NewECA(aca) tca = ca.NewTCA(eca) tlsca = ca.NewTLSCA(eca) } diff --git a/docs/Setup/NodeSDK-setup.md b/docs/Setup/NodeSDK-setup.md index 57ecbee5f03..a987d04c316 100644 --- a/docs/Setup/NodeSDK-setup.md +++ b/docs/Setup/NodeSDK-setup.md @@ -122,11 +122,16 @@ function handleUserRequest(userName, chaincodeID, fcn, args) { // If this user has already been registered and/or enrolled, this will // still succeed because the state is kept in the KeyValStore // (i.e. in '/tmp/keyValStore' in this sample). + // The attributes field is optional but can be used for role-based access control. + // See fabric/sdk/node/test/unit/asset-mgmt-with-dynamic-roles.js as an example. var registrationRequest = { enrollmentID: userName, // Customize account & affiliation account: "bank_a", - affiliation: "00001" + affiliation: "00001", + attributes: [ + { name: "bankAccountId", value: "12345-67890" } + ] }; chain.registerAndEnroll( registrationRequest, function(err, user) { if (err) return console.log("ERROR: %s",err); diff --git a/examples/chaincode/go/asset_management/asset_management.go b/examples/chaincode/go/asset_management/asset_management.go index c059e9155da..6edf1edd720 100644 --- a/examples/chaincode/go/asset_management/asset_management.go +++ b/examples/chaincode/go/asset_management/asset_management.go @@ -264,7 +264,7 @@ func (t *AssetManagementChaincode) Query(stub shim.ChaincodeStubInterface, funct myLogger.Debugf("Query [%s]", function) if function != "query" { - return nil, errors.New("Invalid query function name. Expecting \"query\"") + return nil, errors.New("Invalid query function name. Expecting 'query' but found '" + function + "'") } var err error diff --git a/examples/chaincode/go/asset_management02/asset_management02_test.go b/examples/chaincode/go/asset_management02/asset_management02_test.go index b6871c2315b..14efcb800da 100755 --- a/examples/chaincode/go/asset_management02/asset_management02_test.go +++ b/examples/chaincode/go/asset_management02/asset_management02_test.go @@ -531,7 +531,7 @@ func initMembershipSrvc() { //ca.LogInit(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr, os.Stdout) ca.CacheConfiguration() // Cache configuration aca = ca.NewACA() - eca = ca.NewECA() + eca = ca.NewECA(aca) tca = ca.NewTCA(eca) tlsca = ca.NewTLSCA(eca) diff --git a/examples/chaincode/go/asset_management_with_roles/asset_management_with_roles.go b/examples/chaincode/go/asset_management_with_roles/asset_management_with_roles.go index 20a5eff1233..a1a9222c878 100644 --- a/examples/chaincode/go/asset_management_with_roles/asset_management_with_roles.go +++ b/examples/chaincode/go/asset_management_with_roles/asset_management_with_roles.go @@ -113,7 +113,7 @@ func (t *AssetManagementChaincode) assign(stub shim.ChaincodeStubInterface, args callerRole, err := stub.ReadCertAttribute("role") if err != nil { - fmt.Printf("Error reading attribute [%v] \n", err) + fmt.Printf("Error reading attribute 'role' [%v] \n", err) return nil, fmt.Errorf("Failed fetching caller role. Error was [%v]", err) } @@ -235,7 +235,7 @@ func (t *AssetManagementChaincode) Invoke(stub shim.ChaincodeStubInterface, func // Query callback representing the query of a chaincode func (t *AssetManagementChaincode) Query(stub shim.ChaincodeStubInterface, function string, args []string) ([]byte, error) { if function != "query" { - return nil, errors.New("Invalid query function name. Expecting \"query\"") + return nil, errors.New("Invalid query function name. Expecting 'query' but found '" + function + "'") } var err error diff --git a/examples/chaincode/go/asset_management_with_roles/asset_management_with_roles_test.go b/examples/chaincode/go/asset_management_with_roles/asset_management_with_roles_test.go index d90f50ae1ef..7c087bd6502 100644 --- a/examples/chaincode/go/asset_management_with_roles/asset_management_with_roles_test.go +++ b/examples/chaincode/go/asset_management_with_roles/asset_management_with_roles_test.go @@ -366,7 +366,7 @@ func setup() { func initMembershipSrvc() { ca.CacheConfiguration() // Cache configuration aca = ca.NewACA() - eca = ca.NewECA() + eca = ca.NewECA(aca) tca = ca.NewTCA(eca) tlsca = ca.NewTLSCA(eca) diff --git a/examples/chaincode/go/rbac_tcerts_no_attrs/rbac_test.go b/examples/chaincode/go/rbac_tcerts_no_attrs/rbac_test.go index 19cba9259ec..fe9affdb6a8 100644 --- a/examples/chaincode/go/rbac_tcerts_no_attrs/rbac_test.go +++ b/examples/chaincode/go/rbac_tcerts_no_attrs/rbac_test.go @@ -441,7 +441,7 @@ func setup() { func initMemershipServices() { ca.CacheConfiguration() // Cache configuration - eca = ca.NewECA() + eca = ca.NewECA(nil) tca = ca.NewTCA(eca) tlsca = ca.NewTLSCA(eca) diff --git a/membersrvc/ca/aca.go b/membersrvc/ca/aca.go index c0d83e814fe..9db6b9dd3bf 100644 --- a/membersrvc/ca/aca.go +++ b/membersrvc/ca/aca.go @@ -276,7 +276,10 @@ func (aca *ACA) fetchAttributes(id, affiliation string) ([]*AttributePair, error return attributes, nil } -func (aca *ACA) populateAttributes(attrs []*AttributePair) error { +func (aca *ACA) PopulateAttributes(attrs []*AttributePair) error { + + acaLogger.Debugf("PopulateAttributes: %+v", attrs) + mutex.Lock() defer mutex.Unlock() @@ -285,6 +288,7 @@ func (aca *ACA) populateAttributes(attrs []*AttributePair) error { return dberr } for _, attr := range attrs { + acaLogger.Debugf("attr: %+v", attr) if err := aca.populateAttribute(tx, attr); err != nil { dberr = tx.Rollback() if dberr != nil { @@ -331,7 +335,7 @@ func (aca *ACA) fetchAndPopulateAttributes(id, affiliation string) error { if err != nil { return err } - err = aca.populateAttributes(attrs) + err = aca.PopulateAttributes(attrs) if err != nil { return err } diff --git a/membersrvc/ca/ca.go b/membersrvc/ca/ca.go index 05789be7dbb..3698d9b1f7c 100644 --- a/membersrvc/ca/ca.go +++ b/membersrvc/ca/ca.go @@ -35,6 +35,8 @@ import ( "sync" "time" + gp "google/protobuf" + "github.com/hyperledger/fabric/core/crypto/primitives" "github.com/hyperledger/fabric/flogging" pb "github.com/hyperledger/fabric/membersrvc/protos" @@ -578,11 +580,11 @@ func (ca *CA) validateAndGenerateEnrollID(id, affiliation string, role pb.Role) // registerUser registers a new member with the CA // -func (ca *CA) registerUser(id, affiliation string, role pb.Role, registrar, memberMetadata string, opt ...string) (string, error) { +func (ca *CA) registerUser(id, affiliation string, role pb.Role, attrs []*pb.Attribute, aca *ACA, registrar, memberMetadata string, opt ...string) (string, error) { memberMetadata = removeQuotes(memberMetadata) roleStr, _ := MemberRoleToString(role) - caLogger.Debugf("Received request to register user with id: %s, affiliation: %s, role: %s, registrar: %s, memberMetadata: %s\n", - id, affiliation, roleStr, registrar, memberMetadata) + caLogger.Debugf("Received request to register user with id: %s, affiliation: %s, role: %s, attrs: %+v, registrar: %s, memberMetadata: %s\n", + id, affiliation, roleStr, attrs, registrar, memberMetadata) var enrollID, tok string var err error @@ -606,11 +608,21 @@ func (ca *CA) registerUser(id, affiliation string, role pb.Role, registrar, memb if err != nil { return "", err } + tok, err = ca.registerUserWithEnrollID(id, enrollID, role, memberMetadata, opt...) if err != nil { return "", err } - return tok, nil + + if attrs != nil && aca != nil { + var pairs []*AttributePair + pairs, err = toAttributePairs(id, affiliation, attrs) + if err == nil { + err = aca.PopulateAttributes(pairs) + } + } + + return tok, err } // registerUserWithEnrollID registers a new user and its enrollmentID, role and state @@ -870,25 +882,36 @@ func (mm *MemberMetadata) canRegister(registrar string, newRole string, newMembe caLogger.Debugf("MM.canRegister: role %s can't be registered by %s\n", newRole, registrar) return errors.New("member " + registrar + " may not register member of type " + newRole) } + // The registrar privileges that are being registered must not be larger than the registrar's if newMemberMetadata == nil { // Not requesting registrar privileges for this member, so we are OK caLogger.Debug("MM.canRegister: not requesting registrar privileges") return nil } - return strsContained(newMemberMetadata.Registrar.Roles, mm.Registrar.DelegateRoles, registrar, "delegateRoles") + + // Make sure this registrar is not delegating an invalid role + err := checkDelegateRoles(newMemberMetadata.Registrar.Roles, mm.Registrar.DelegateRoles, registrar) + if err != nil { + caLogger.Debug("MM.canRegister: checkDelegateRoles failure") + return err + } + + // Can register OK + caLogger.Debug("MM.canRegister: OK") + return nil } // Return an error if all strings in 'strs1' are not contained in 'strs2' -func strsContained(strs1 []string, strs2 []string, registrar string, field string) error { - caLogger.Debugf("CA.strsContained: registrar=%s, field=%s, strs1=%+v, strs2=%+v\n", registrar, field, strs1, strs2) +func checkDelegateRoles(strs1 []string, strs2 []string, registrar string) error { + caLogger.Debugf("CA.checkDelegateRoles: registrar=%s, strs1=%+v, strs2=%+v\n", registrar, strs1, strs2) for _, s := range strs1 { if !strContained(s, strs2) { - caLogger.Debugf("CA.strsContained: no: %s not in %+v\n", s, strs2) - return errors.New("user " + registrar + " may not register " + field + " " + s) + caLogger.Debugf("CA.checkDelegateRoles: no: %s not in %+v\n", s, strs2) + return errors.New("user " + registrar + " may not register delegateRoles " + s) } } - caLogger.Debug("CA.strsContained: ok") + caLogger.Debug("CA.checkDelegateRoles: ok") return nil } @@ -902,6 +925,16 @@ func strContained(str string, strs []string) bool { return false } +// Return true if 'str' is prefixed by any string in 'strs'; otherwise return false +func isPrefixed(str string, strs []string) bool { + for _, s := range strs { + if strings.HasPrefix(str, s) { + return true + } + } + return false +} + // convert a role to a string func role2String(role int) string { if role == int(pb.Role_CLIENT) { @@ -928,3 +961,30 @@ func removeQuotes(str string) string { caLogger.Debugf("removeQuotes: %s\n", str) return str } + +// Convert the protobuf array of attributes to the AttributePair array format +// as required by the ACA code to populate the table +func toAttributePairs(id, affiliation string, attrs []*pb.Attribute) ([]*AttributePair, error) { + var pairs = make([]*AttributePair, 0) + for _, attr := range attrs { + vals := []string{id, affiliation, attr.Name, attr.Value, attr.NotBefore, attr.NotAfter} + pair, err := NewAttributePair(vals, nil) + if err != nil { + return nil, err + } + pairs = append(pairs, pair) + } + caLogger.Debugf("toAttributePairs: id=%s, affiliation=%s, attrs=%v, pairs=%v\n", + id, affiliation, attrs, pairs) + return pairs, nil +} + +func convertTime(ts *gp.Timestamp) time.Time { + var t time.Time + if ts == nil { + t = time.Unix(0, 0).UTC() + } else { + t = time.Unix(ts.Seconds, int64(ts.Nanos)).UTC() + } + return t +} diff --git a/membersrvc/ca/eca.go b/membersrvc/ca/eca.go index 9fcb5e129ec..43ec9522621 100644 --- a/membersrvc/ca/eca.go +++ b/membersrvc/ca/eca.go @@ -48,6 +48,7 @@ var ( // type ECA struct { *CA + aca *ACA obcKey []byte obcPriv, obcPub []byte gRPCServer *grpc.Server @@ -59,8 +60,8 @@ func initializeECATables(db *sql.DB) error { // NewECA sets up a new ECA. // -func NewECA() *ECA { - eca := &ECA{CA: NewCA("eca", initializeECATables)} +func NewECA(aca *ACA) *ECA { + eca := &ECA{CA: NewCA("eca", initializeECATables), aca: aca} flogging.LoggingInit("eca") { @@ -152,7 +153,7 @@ func (eca *ECA) populateUsersTable() { } } } - eca.registerUser(id, affiliation, pb.Role(role), registrar, memberMetadata, vals[1]) + eca.registerUser(id, affiliation, pb.Role(role), nil, eca.aca, registrar, memberMetadata, vals[1]) } } diff --git a/membersrvc/ca/ecaa.go b/membersrvc/ca/ecaa.go index 40af1c1fe06..15cd3e6f0ed 100644 --- a/membersrvc/ca/ecaa.go +++ b/membersrvc/ca/ecaa.go @@ -60,7 +60,7 @@ func (ecaa *ECAA) RegisterUser(ctx context.Context, in *pb.RegisterUserReq) (*pb } jsonStr := string(json) ecaaLogger.Debugf("gRPC ECAA:RegisterUser: json=%s", jsonStr) - tok, err := ecaa.eca.registerUser(in.Id.Id, in.Affiliation, in.Role, registrarID, jsonStr) + tok, err := ecaa.eca.registerUser(in.Id.Id, in.Affiliation, in.Role, in.Attributes, ecaa.eca.aca, registrarID, jsonStr) // Return the one-time password return &pb.Token{Tok: []byte(tok)}, err @@ -105,7 +105,7 @@ func (ecaa *ECAA) checkRegistrarSignature(in *pb.RegisterUserReq) error { // Check the signature if ecdsa.Verify(cert.PublicKey.(*ecdsa.PublicKey), hash.Sum(nil), r, s) == false { // Signature verification failure - ecaaLogger.Debugf("ECAA.checkRegistrarSignature: failure for %s", registrar) + ecaaLogger.Debugf("ECAA.checkRegistrarSignature: failure for %s (len=%d): %+v", registrar, len(raw), in) return errors.New("Signature verification failed.") } diff --git a/membersrvc/ca/membersrvc_test.go b/membersrvc/ca/membersrvc_test.go index 6a0ec0a756f..a294f252663 100644 --- a/membersrvc/ca/membersrvc_test.go +++ b/membersrvc/ca/membersrvc_test.go @@ -70,7 +70,7 @@ func setupTestConfig() { func initPKI() { CacheConfiguration() // Cache configuration aca = NewACA() - eca = NewECA() + eca = NewECA(aca) tca = NewTCA(eca) } diff --git a/membersrvc/ca/tca_test.go b/membersrvc/ca/tca_test.go index a0b38e1941d..12008c7ecb8 100644 --- a/membersrvc/ca/tca_test.go +++ b/membersrvc/ca/tca_test.go @@ -155,16 +155,17 @@ func initTCA() (*TCA, error) { } CacheConfiguration() // Cache configuration - eca := NewECA() - if eca == nil { - return nil, fmt.Errorf("Could not create a new ECA") - } aca := NewACA() if aca == nil { return nil, fmt.Errorf("Could not create a new ACA") } + eca := NewECA(aca) + if eca == nil { + return nil, fmt.Errorf("Could not create a new ECA") + } + tca := NewTCA(eca) if tca == nil { return nil, fmt.Errorf("Could not create a new TCA") diff --git a/membersrvc/ca/tlsca_test.go b/membersrvc/ca/tlsca_test.go index de890a9b704..ff68a524870 100644 --- a/membersrvc/ca/tlsca_test.go +++ b/membersrvc/ca/tlsca_test.go @@ -63,7 +63,7 @@ func TestTLS(t *testing.T) { func startTLSCA(t *testing.T) { CacheConfiguration() // Cache configuration - ecaS = NewECA() + ecaS = NewECA(nil) tlscaS = NewTLSCA(ecaS) var opts []grpc.ServerOption diff --git a/membersrvc/protos/ca.pb.go b/membersrvc/protos/ca.pb.go index a5d9b6d902a..61c2bf8a10f 100644 --- a/membersrvc/protos/ca.pb.go +++ b/membersrvc/protos/ca.pb.go @@ -19,6 +19,7 @@ It has these top-level messages: Signature Registrar RegisterUserReq + Attribute ReadUserSetReq User UserSet @@ -336,11 +337,12 @@ func (m *Registrar) GetId() *Identity { } type RegisterUserReq struct { - Id *Identity `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` - Role Role `protobuf:"varint,2,opt,name=role,enum=protos.Role" json:"role,omitempty"` - Affiliation string `protobuf:"bytes,4,opt,name=affiliation" json:"affiliation,omitempty"` - Registrar *Registrar `protobuf:"bytes,5,opt,name=registrar" json:"registrar,omitempty"` - Sig *Signature `protobuf:"bytes,6,opt,name=sig" json:"sig,omitempty"` + Id *Identity `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` + Role Role `protobuf:"varint,2,opt,name=role,enum=protos.Role" json:"role,omitempty"` + Attributes []*Attribute `protobuf:"bytes,3,rep,name=attributes" json:"attributes,omitempty"` + Affiliation string `protobuf:"bytes,4,opt,name=affiliation" json:"affiliation,omitempty"` + Registrar *Registrar `protobuf:"bytes,5,opt,name=registrar" json:"registrar,omitempty"` + Sig *Signature `protobuf:"bytes,6,opt,name=sig" json:"sig,omitempty"` } func (m *RegisterUserReq) Reset() { *m = RegisterUserReq{} } @@ -354,6 +356,13 @@ func (m *RegisterUserReq) GetId() *Identity { return nil } +func (m *RegisterUserReq) GetAttributes() []*Attribute { + if m != nil { + return m.Attributes + } + return nil +} + func (m *RegisterUserReq) GetRegistrar() *Registrar { if m != nil { return m.Registrar @@ -368,6 +377,17 @@ func (m *RegisterUserReq) GetSig() *Signature { return nil } +type Attribute struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value" json:"value,omitempty"` + NotBefore string `protobuf:"bytes,3,opt,name=notBefore" json:"notBefore,omitempty"` + NotAfter string `protobuf:"bytes,4,opt,name=notAfter" json:"notAfter,omitempty"` +} + +func (m *Attribute) Reset() { *m = Attribute{} } +func (m *Attribute) String() string { return proto.CompactTextString(m) } +func (*Attribute) ProtoMessage() {} + type ReadUserSetReq struct { Req *Identity `protobuf:"bytes,1,opt,name=req" json:"req,omitempty"` Role Role `protobuf:"varint,2,opt,name=role,enum=protos.Role" json:"role,omitempty"` diff --git a/membersrvc/protos/ca.proto b/membersrvc/protos/ca.proto index a7a0feb6927..7690e62f314 100644 --- a/membersrvc/protos/ca.proto +++ b/membersrvc/protos/ca.proto @@ -139,19 +139,27 @@ enum Role { } message Registrar { - Identity id = 1; // The identity of the registrar - repeated string roles = 2; // Roles that the registrar can register - repeated string delegateRoles = 3; // Roles that the registrar can give to another to register + Identity id = 1; // The identity of the registrar + repeated string roles = 2; // Roles that the registrar can register + repeated string delegateRoles = 3; // Roles that the registrar can give to another to register } message RegisterUserReq { Identity id = 1; Role role = 2; - string affiliation = 4; // Skipping field number 3 to maintain backward compatibility. It was used before for the primary account. + repeated Attribute attributes = 3; + string affiliation = 4; Registrar registrar = 5; Signature sig = 6; } +message Attribute { + string name = 1; + string value = 2; + string notBefore = 3; + string notAfter = 4; +} + message ReadUserSetReq { Identity req = 1; Role role = 2; // bitmask diff --git a/membersrvc/server.go b/membersrvc/server.go index 180ca51bdbb..763d90f4c8a 100644 --- a/membersrvc/server.go +++ b/membersrvc/server.go @@ -73,7 +73,7 @@ func main() { aca := ca.NewACA() defer aca.Stop() - eca := ca.NewECA() + eca := ca.NewECA(aca) defer eca.Stop() tca := ca.NewTCA(eca) diff --git a/sdk/node/bin/run-unit-tests.sh b/sdk/node/bin/run-unit-tests.sh index cc0baaa02aa..8721057988f 100755 --- a/sdk/node/bin/run-unit-tests.sh +++ b/sdk/node/bin/run-unit-tests.sh @@ -31,8 +31,16 @@ init() { FABRIC=$GOPATH/src/github.com/hyperledger/fabric LOGDIR=/tmp/node-sdk-unit-test MSEXE=$FABRIC/build/bin/membersrvc - MSLOGFILE=$LOGDIR/membersrvc.log + MSEXE2=$FABRIC/build/image/membersrvc/bin/membersrvc + if [ ! -f $MSEXE -a -f $MSEXE2 ]; then + MSEXE=$MSEXE2 + fi PEEREXE=$FABRIC/build/bin/peer + PEEREXE2=$FABRIC/build/image/peer/bin/peer + if [ ! -f $PEEREXE -a -f $PEEREXE2 ]; then + PEEREXE=$PEEREXE2 + fi + MSLOGFILE=$LOGDIR/membersrvc.log PEERLOGFILE=$LOGDIR/peer.log UNITTEST=$FABRIC/sdk/node/test/unit EXAMPLES=$FABRIC/examples/chaincode/go @@ -73,6 +81,7 @@ runTests() { runChainTests runAssetMgmtTests runAssetMgmtWithRolesTests + runAssetMgmtWithDynamicRolesTests echo "End running tests in network mode" } @@ -148,10 +157,8 @@ startExampleInDevMode() { exit 1; fi EXE=$SRCDIR/$1 - if [ ! -f $EXE ]; then - cd $SRCDIR - go build - fi + cd $SRCDIR + go build export CORE_CHAINCODE_ID_NAME=$2 export CORE_PEER_ADDRESS=0.0.0.0:7051 startProcess "$EXE" "${EXE}.log" "$1" @@ -208,6 +215,18 @@ runAssetMgmtWithRolesTests() { echo "END running asset management with roles tests" } +runAssetMgmtWithDynamicRolesTests() { + echo "BEGIN running asset management with dynamic roles tests ..." + preExample asset_management_with_roles mycc3 + node $UNITTEST/asset-mgmt-with-dynamic-roles.js + if [ $? -ne 0 ]; then + echo "ERROR running asset management with dynamic roles tests!" + NODE_ERR_CODE=1 + fi + postExample asset_management_with_roles + echo "END running asset management with dynamic roles tests" +} + # start process # $1 is executable path with any args # $2 is the log file diff --git a/sdk/node/src/hfc.ts b/sdk/node/src/hfc.ts index 1a420e669ec..d2858d37d85 100644 --- a/sdk/node/src/hfc.ts +++ b/sdk/node/src/hfc.ts @@ -159,6 +159,8 @@ export interface RegistrationRequest { roles?:string[]; // Affiliation for a user affiliation:string; + // The attribute names and values to grant to this member + attributes?:Attribute[]; // 'registrar' enables this identity to register other members with types // and can delegate the 'delegationRoles' roles registrar?:{ @@ -169,6 +171,15 @@ export interface RegistrationRequest { }; } +// An attribute consisting of a name and value +export interface Attribute { + // The attribute name + name:string; + // The attribute value + value:string; +} + +// An enrollment request export interface EnrollmentRequest { // The enrollment ID enrollmentID:string; @@ -290,7 +301,7 @@ export class Certificate { /** * Enrollment certificate. */ -export class ECert extends Certificate { +class ECert extends Certificate { constructor(public cert:Buffer, public privateKey:any) { @@ -2309,10 +2320,25 @@ class MemberServicesImpl implements MemberServices { debug("MemberServicesImpl.register: req=%j", req); if (!req.enrollmentID) return cb(new Error("missing req.enrollmentID")); if (!registrar) return cb(new Error("chain registrar is not set")); + // Create proto request let protoReq = new _caProto.RegisterUserReq(); protoReq.setId({id:req.enrollmentID}); protoReq.setRole(rolesToMask(req.roles)); protoReq.setAffiliation(req.affiliation); + let attrs = req.attributes; + if (Array.isArray(attrs)) { + let pattrs = []; + for (var i = 0; i < attrs.length; i++) { + var attr = attrs[i]; + var pattr = new _caProto.Attribute(); + if (attr.name) pattr.setName(attr.name); + if (attr.value) pattr.setValue(attr.value); + if (attr.notBefore) pattr.setNotBefore(attr.notBefore); + if (attr.notAfter) pattr.setNotAfter(attr.notAfter); + pattrs.push(pattr); + } + protoReq.setAttributes(pattrs); + } // Create registrar info let protoRegistrar = new _caProto.Registrar(); protoRegistrar.setId({id:registrar.getName()}); diff --git a/sdk/node/test/unit/asset-mgmt-with-dynamic-roles.js b/sdk/node/test/unit/asset-mgmt-with-dynamic-roles.js new file mode 100644 index 00000000000..0611cd8c98d --- /dev/null +++ b/sdk/node/test/unit/asset-mgmt-with-dynamic-roles.js @@ -0,0 +1,230 @@ +/** + * Copyright 2016 IBM + * + * 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. + */ +/** + * Licensed Materials - Property of IBM + * © Copyright IBM Corp. 2016 + */ + +/** + * Simple asset management use case where authentication is performed + * with the help of TCerts only (use-case 1) or attributes only (use-case 2).*/ + +var hfc = require('../..'); +var test = require('tape'); +var util = require('util'); +var crypto = require('../../lib/crypto'); + +var chain, chaincodeID; +var chaincodeName = "mycc3"; +var deployer, alice, bob, assigner; +var aliceAccount = "12345-56789"; +var bobAccount = "23456-67890"; +var devMode = process.env.DEPLOY_MODE == 'dev'; + +// Create the chain and enroll users as deployer, assigner, and nonAssigner (who doesn't have privilege to assign. +function setup(cb) { + console.log("initializing ..."); + chain = hfc.newChain("testChain"); + chain.setKeyValStore(hfc.newFileKeyValStore("/tmp/keyValStore")); + chain.setMemberServicesUrl("grpc://localhost:7054"); + chain.addPeer("grpc://localhost:7051"); + if (devMode) chain.setDevMode(true); + console.log("enrolling deployer ..."); + chain.enroll("WebAppAdmin", "DJY27pEnl16d", function (err, user) { + if (err) return cb(err); + deployer = user; + chain.setRegistrar(user); + console.log("enrolling assigner2 ..."); + registerAndEnroll("assigner2",[{name:'role',value:'assigner'}], function(err,user) { + if (err) return cb(err); + assigner = user; + console.log("enrolling alice2 ..."); + registerAndEnroll("alice2",[{name:'role',value:'client'},{name:'account',value:aliceAccount}], function(err,user) { + if (err) return cb(err); + alice = user; + console.log("enrolling bob2 ..."); + registerAndEnroll("bob2",[{name:'role',value:'client'},{name:'account',value:bobAccount}], function(err,user) { + if (err) return cb(err); + bob = user; + return deploy(cb); + }); + }); + }); + }); +} + +// Deploy assetmgmt_with_roles with the name of the assigner role in the metadata +function deploy(cb) { + console.log("deploying with the role name 'assigner' in metadata ..."); + var req = { + fcn: "init", + args: [], + metadata: new Buffer("assigner") + }; + if (devMode) { + req.chaincodeName = chaincodeName; + } else { + req.chaincodePath = "github.com/asset_management_with_roles/"; + } + var tx = deployer.deploy(req); + tx.on('submitted', function (results) { + console.log("deploy submitted: %j", results); + }); + tx.on('complete', function (results) { + console.log("deploy complete: %j", results); + chaincodeID = results.chaincodeID; + console.log("chaincodeID:" + chaincodeID); + return cb(); + }); + tx.on('error', function (err) { + console.log("deploy error: %j", err.toString()); + return cb(err); + }); +} + +function assignOwner(user,owner,cb) { + var req = { + chaincodeID: chaincodeID, + fcn: "assign", + args: ["MyAsset",owner], + attrs: ['role'] + }; + console.log("assign: invoking %j",req); + var tx = user.invoke(req); + tx.on('submitted', function (results) { + console.log("assign transaction ID: %j", results); + }); + tx.on('complete', function (results) { + console.log("assign invoke complete: %j", results); + return cb(); + }); + tx.on('error', function (err) { + console.log("assign invoke error: %j", err); + return cb(err); + }); +} + +// Check to see if the owner of the asset is +function checkOwner(user,ownerAccount,cb) { + var req = { + chaincodeID: chaincodeID, + fcn: "query", + args: ["MyAsset"] + }; + console.log("query: querying %j",req); + var tx = user.query(req); + tx.on('complete', function (results) { + var realOwner = results.result; + //console.log("realOwner: " + realOwner); + + if (ownerAccount == results.result) { + console.log("correct owner: %s",ownerAccount); + return cb(); + } else { + return cb(new Error(util.format("incorrect owner: expected=%s, real=%s",ownerAccount,realOwner))); + } + }); + tx.on('error', function (err) { + console.log("assign invoke error: %j", err); + return cb(err); + }); +} + +test('setup asset management with roles', function (t) { + t.plan(1); + + console.log("setup asset management with roles"); + + setup(function(err) { + if (err) { + t.fail("error: "+err.toString()); + // Exit the test script after a failure + process.exit(1); + } else { + t.pass("setup successful"); + } + }); +}); + +test('assign asset management with roles', function (t) { + t.plan(1); + alice.getUserCert(["role", "account"], function (err, aliceCert) { + if (err) { + fail(t, "Failed getting Application certificate for Alice."); + // Exit the test script after a failure + process.exit(1); + } + assignOwner(assigner, aliceCert.encode().toString('base64'), function(err) { + if (err) { + t.fail("error: "+err.toString()); + // Exit the test script after a failure + process.exit(1); + } else { + checkOwner(assigner, aliceAccount, function(err) { + if(err){ + t.fail("error: "+err.toString()); + // Exit the test script after a failure + process.exit(1); + } else { + t.pass("assign successful"); + } + }); + } + }); + }); +}); + +test('not assign asset management with roles', function (t) { + t.plan(1); + + bob.getUserCert(["role", "account"], function (err, bobCert) { + if (err) { + fail(t, "Failed getting Application certificate for Alice."); + // Exit the test script after a failure + process.exit(1); + } + assignOwner(alice, bobCert.encode().toString('base64'), function(err) { + if (err) { + t.fail("error: "+err.toString()); + // Exit the test script after a failure + process.exit(1); + } else { + checkOwner(alice, bobAccount, function(err) { + if(err){ + t.pass("assign successful"); + } else { + err = new Error ("this user should not have been allowed to assign"); + t.fail("error: "+err.toString()); + // Exit the test script after a failure + process.exit(1); + } + }); + } + }); + }); +}); + +function registerAndEnroll(name, attrs, cb) { + console.log("registerAndEnroll name=%s attrs=%j",name,attrs); + var registrationRequest = { + roles: [ 'client' ], + enrollmentID: name, + affiliation: "bank_a", + attributes: attrs + }; + chain.registerAndEnroll(registrationRequest,cb); +} + diff --git a/sdk/node/test/unit/registrar.js b/sdk/node/test/unit/registrar.js index 61489f98954..726b3f33c42 100644 --- a/sdk/node/test/unit/registrar.js +++ b/sdk/node/test/unit/registrar.js @@ -23,7 +23,7 @@ var test = require('tape'); var util = require('util'); var fs = require('fs'); -var keyValStorePath = "/tmp/keyValStore" +var keyValStorePath = "/tmp/keyValStore"; var keyValStorePath2 = keyValStorePath + "2"; // @@ -56,60 +56,97 @@ test('enroll again', function (t) { }); }); -// The registrar test +var chain,admin,webAdmin,webUser; + +// STEP1: Init the chain and enroll admin function registrarTest(cb) { - console.log("testRegistrar"); + console.log("registrarTest: STEP 1"); // // Create and configure the test chain // - var chain = hfc.newChain("testChain"); - var expect=""; - var found=""; - + chain = hfc.newChain("testChain"); chain.setKeyValStore(hfc.newFileKeyValStore(keyValStorePath)); chain.setMemberServicesUrl("grpc://localhost:7054"); - chain.enroll("admin", "Xurw3yU9zI0l", function (err, admin) { + chain.enroll("admin", "Xurw3yU9zI0l", function (err, user) { if (err) return cb(err); + admin = user; chain.setRegistrar(admin); - // Register and enroll webAdmin - registerAndEnroll("webAdmin", "client", {roles:['client']}, chain, function(err,webAdmin) { - if (err) return cb(err); - chain.setRegistrar(webAdmin); - registerAndEnroll("webUser", "client", null, chain, function(err, webUser) { - if (err) return cb(err); - registerAndEnroll("auditor", "auditor", null, chain, function(err, auditor) { - if (!err) return cb(err); - expect="webAdmin may not register member of type auditor"; - found = (err.toString()).match(expect); - if (!(found==expect)) cb(err); - registerAndEnroll("validator", "validator", null, chain, function(err, validator) { - if (!err) return cb(err); - expect="webAdmin may not register member of type validator"; - found = (err.toString()).match(expect); - if (!(found==expect)) cb(err); - chain.setRegistrar(webUser); - registerAndEnroll("webUser2", "client", null, chain, function(err) { - if (!err) return cb(Error("webUser should not be allowed to register a client")); - expect="webUser may not register member of type client"; - found = (err.toString()).match(expect); - if (!(found==expect)) cb(err); - return cb(); - }); - }); - }); - }); - }); + registrarTestStep2(cb); + }); +} + +// STEP 2: Register and enroll webAdmin and set as register +function registrarTestStep2(cb) { + console.log("registrarTest: STEP 2"); + registerAndEnroll("webAdmin", "client", null, {roles:['client']}, chain, function(err,user) { + if (err) return cb(err); + webAdmin = user; + chain.setRegistrar(webAdmin); + registrarTestStep3(cb); }); } -// Register and enroll user 'name' with role 'r' with registrar info 'registrar' for chain 'chain' -function registerAndEnroll(name, r, registrar, chain, cb) { +// STEP 3: Register webUser with an attribute" +function registrarTestStep3(cb) { + console.log("registrarTest: STEP 3"); + var attrs; + attrs = [{name:'foo',value:'bar'}]; + registerAndEnroll("webUser", "client", attrs, null, chain, function(err, user) { + if (err) return cb(err); + webUser = user; + registrarTestStep4(cb); + }); +} + +// STEP 4: Negative test attempting to register an auditor type by webAdmin +function registrarTestStep4(cb) { + console.log("registrarTest: STEP 4"); + registerAndEnroll("auditorUser", "auditor", null, null, chain, function(err) { + if (!err) return cb(Error("webAdmin should not have been allowed to register member of type auditor")); + err = checkErr(err,"webAdmin may not register member of type auditor"); + if (err) return cb(err); + registrarTestStep5(cb); + }); +} + +// STEP 5: Negative test attempting to register a validator type by webAdmin +function registrarTestStep5(cb) { + console.log("registrarTest: STEP 5"); + registerAndEnroll("validatorUser", "validator", null, null, chain, function(err) { + if (!err) return cb(Error("webAdmin should not have been allowed to register member of type validator")); + err = checkErr(err,"webAdmin may not register member of type validator"); + if (err) return cb(err); + registrarTestStep6(cb); + }); +} + +// STEP 6: Negative test attempting to register a client type by webUser +function registrarTestStep6(cb) { + console.log("registrarTest: STEP 6"); + chain.setRegistrar(webUser); + registerAndEnroll("webUser2", "client", null, webUser, chain, function(err) { + if (!err) return cb(Error("webUser should not be allowed to register a member of type client")); + err = checkErr(err,"webUser may not register member of type client"); + if (err) return cb(err); + registrarTestDone(cb); + }); +} + +// Final step: success +function registrarTestDone(cb) { + console.log("registrarTest: Done"); + return cb(); +} + +// Register and enroll user 'name' with role 'role' with registrar info 'registrar' for chain 'chain' +function registerAndEnroll(name, role, attributes, registrar, chain, cb) { console.log("registerAndEnroll %s",name); // User is not enrolled yet, so perform both registration and enrollment var registrationRequest = { - roles: [ r ], + roles: [ role ], enrollmentID: name, affiliation: "bank_a", + attributes: attributes, registrar: registrar }; chain.registerAndEnroll(registrationRequest,cb); @@ -150,6 +187,16 @@ function rmdir(path) { } } +function checkErr(err,expect) { + err = err.toString(); + var found = err.match(expect); + if (!found || found.length === 0) { + err = new Error(util.format("Incorrect error: expecting '%s' but found '%s'",expect,err)); + return err; + } + return null; +} + function pass(t, msg) { t.pass("Success: [" + msg + "]"); t.end();