From 54c6ea2ea47e626188f21883a8c396634d6640e0 Mon Sep 17 00:00:00 2001 From: Simon Stone Date: Wed, 12 Jul 2017 13:10:09 +0100 Subject: [PATCH 1/7] Initial drop of identity registry (#1539) --- packages/composer-client/api.txt | 1 + packages/composer-client/changelog.txt | 3 + .../lib/businessnetworkconnection.js | 111 ++++- .../test/businessnetworkconnection.js | 138 +++++- packages/composer-common/lib/modelmanager.js | 38 ++ .../composer-runtime-hlfv1/identityservice.go | 121 +++-- .../lib/compiledscriptbundle.js | 5 +- packages/composer-runtime/lib/context.js | 119 +++-- .../composer-runtime/lib/engine.identities.js | 71 ++- packages/composer-runtime/lib/engine.js | 21 +- .../composer-runtime/lib/engine.logging.js | 2 +- packages/composer-runtime/lib/httpservice.js | 6 +- .../composer-runtime/lib/identitymanager.js | 429 +++++++++++++---- .../composer-runtime/lib/identityservice.js | 34 +- .../composer-runtime/lib/queryexecutor.js | 2 +- .../composer-runtime/lib/transactionlogger.js | 6 +- packages/composer-runtime/test/context.js | 196 +++++--- .../test/engine.identities.js | 68 ++- packages/composer-runtime/test/engine.js | 28 +- .../composer-runtime/test/identitymanager.js | 454 +++++++++++++----- .../composer-runtime/test/identityservice.js | 34 +- .../composer-systests/systest/identities.js | 4 +- 22 files changed, 1452 insertions(+), 439 deletions(-) diff --git a/packages/composer-client/api.txt b/packages/composer-client/api.txt index a7619c43f3..59b72cc7e8 100644 --- a/packages/composer-client/api.txt +++ b/packages/composer-client/api.txt @@ -24,6 +24,7 @@ class BusinessNetworkConnection extends EventEmitter { + Promise query(Object) + Promise ping() + Promise issueIdentity(string,object,boolean) + + Promise bindIdentity(string) + Promise revokeIdentity(string) } class ParticipantRegistry extends Registry { diff --git a/packages/composer-client/changelog.txt b/packages/composer-client/changelog.txt index 9e2dcb3813..42108c1389 100644 --- a/packages/composer-client/changelog.txt +++ b/packages/composer-client/changelog.txt @@ -12,6 +12,9 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 0.9.2 {465d5a96640dce8240a392d058e06fe5} 2017-07-11 +- Added bindIdentity + Version 0.9.0 {d6ebf2b722345ee0f7dac56f42730f12} 2017-06-26 - Added buildQuery and query - Removed deprecated BusinesNetworkConnection.existsAssetRegistry method diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js index 4c10c8e111..bace44d451 100644 --- a/packages/composer-client/lib/businessnetworkconnection.js +++ b/packages/composer-client/lib/businessnetworkconnection.js @@ -322,7 +322,7 @@ class BusinessNetworkConnection extends EventEmitter { }) .then((securityContext) => { this.securityContext = securityContext; - return this.connection.ping(this.securityContext); + return this.ping(); }) .then(() => { return Util.queryChainCode(this.securityContext, 'getBusinessNetwork', []); @@ -525,16 +525,64 @@ class BusinessNetworkConnection extends EventEmitter { * been tested. The promise will be rejected if the version is incompatible. */ ping() { + const method = 'ping'; + LOG.entry(method); + return this.pingInner() + .catch((error) => { + if (error.message.match(/ACTIVATION_REQUIRED/)) { + LOG.debug(method, 'Activation required, activating ...'); + return this.activate() + .then(() => { + return this.pingInner(); + }); + } + throw error; + }) + .then((result) => { + LOG.exit(method, result); + return result; + }); + } + + /** + * Test the connection to the runtime and verify that the version of the + * runtime is compatible with this level of the client node.js module. + * @private + * @return {Promise} A promise that will be fufilled when the connection has + * been tested. The promise will be rejected if the version is incompatible. + */ + pingInner() { + const method = 'pingInner'; + LOG.entry(method); Util.securityCheck(this.securityContext); - return this.connection.ping(this.securityContext); + return this.connection.ping(this.securityContext) + .then((result) => { + LOG.exit(method, result); + return result; + }); } /** - * Issue an identity with the specified user ID and map it to the specified + * Activate the current identity on the currently connected business network. + * @private + * @return {Promise} A promise that will be fufilled when the connection has + * been tested. The promise will be rejected if the version is incompatible. + */ + activate() { + const method = 'activate'; + LOG.entry(method); + return Util.invokeChainCode(this.securityContext, 'activateIdentity', []) + .then(() => { + LOG.exit(method); + }); + } + + /** + * Issue an identity with the specified name and map it to the specified * participant. * @param {Resource|string} participant The participant, or the fully qualified * identifier of the participant. The participant must already exist. - * @param {string} userID The user ID for the identity. + * @param {string} identityName The name for the new identity. * @param {object} [options] Options for the new identity. * @param {boolean} [options.issuer] Whether or not the new identity should have * permissions to create additional new identities. False by default. @@ -543,13 +591,13 @@ class BusinessNetworkConnection extends EventEmitter { * the participant does not exist, or if the identity is already mapped to * another participant. */ - issueIdentity(participant, userID, options) { + issueIdentity(participant, identityName, options) { const method = 'issueIdentity'; - LOG.entry(method, participant, userID); + LOG.entry(method, participant, identityName); if (!participant) { throw new Error('participant not specified'); - } else if (!userID) { - throw new Error('userID not specified'); + } else if (!identityName) { + throw new Error('identityName not specified'); } let participantFQI; if (participant instanceof Resource) { @@ -558,9 +606,9 @@ class BusinessNetworkConnection extends EventEmitter { participantFQI = participant; } Util.securityCheck(this.securityContext); - return this.connection.createIdentity(this.securityContext, userID, options) + return this.connection.createIdentity(this.securityContext, identityName, options) .then((identity) => { - return Util.invokeChainCode(this.securityContext, 'addParticipantIdentity', [participantFQI, userID]) + return Util.invokeChainCode(this.securityContext, 'issueIdentity', [participantFQI, identityName]) .then(() => { LOG.exit(method, identity); return identity; @@ -568,24 +616,55 @@ class BusinessNetworkConnection extends EventEmitter { }); } + /** + * Bind an existing identity to the specified participant. + * @param {Resource|string} participant The participant, or the fully qualified + * identifier of the participant. The participant must already exist. + * @param {string} certificate The certificate for the existing identity. + * @return {Promise} A promise that will be fulfilled when the identity has + * been added to the specified participant. The promise will be rejected if + * the participant does not exist, or if the identity is already mapped to + * another participant. + */ + bindIdentity(participant, certificate) { + const method = 'bindIdentity'; + LOG.entry(method, participant, certificate); + if (!participant) { + throw new Error('participant not specified'); + } else if (!certificate) { + throw new Error('certificate not specified'); + } + let participantFQI; + if (participant instanceof Resource) { + participantFQI = participant.getFullyQualifiedIdentifier(); + } else { + participantFQI = participant; + } + Util.securityCheck(this.securityContext); + return Util.invokeChainCode(this.securityContext, 'bindIdentity', [participantFQI, certificate]) + .then(() => { + LOG.exit(method); + }); + } + /** * Revoke the specified identity by removing any existing mapping to a participant. - * @param {string} identity The identity, for example the enrollment ID. + * @param {string} identityId The identity, for example the enrollment ID. * @return {Promise} A promise that will be fulfilled when the identity has * been removed from the specified participant. The promise will be rejected if * the participant does not exist, or if the identity is not mapped to the * participant. */ - revokeIdentity(identity) { + revokeIdentity(identityId) { const method = 'revokeIdentity'; - LOG.entry(method, identity); - if (!identity) { - throw new Error('identity not specified'); + LOG.entry(method, identityId); + if (!identityId) { + throw new Error('identityId not specified'); } Util.securityCheck(this.securityContext); // It is not currently possible to revoke the certificate, so we just call // the runtime to remove the mapping. - return Util.invokeChainCode(this.securityContext, 'removeIdentity', [identity]) + return Util.invokeChainCode(this.securityContext, 'revokeIdentity', [identityId]) .then(() => { LOG.exit(method); }); diff --git a/packages/composer-client/test/businessnetworkconnection.js b/packages/composer-client/test/businessnetworkconnection.js index ec7d0fbb1b..660c660d08 100644 --- a/packages/composer-client/test/businessnetworkconnection.js +++ b/packages/composer-client/test/businessnetworkconnection.js @@ -60,6 +60,7 @@ describe('BusinessNetworkConnection', () => { sandbox = sinon.sandbox.create(); mockSecurityContext = sinon.createStubInstance(SecurityContext); mockConnection = sinon.createStubInstance(Connection); + mockSecurityContext.getConnection.returns(mockConnection); mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); businessNetworkConnection = new BusinessNetworkConnection(); businessNetworkConnection.businessNetwork = mockBusinessNetworkDefinition; @@ -858,6 +859,83 @@ describe('BusinessNetworkConnection', () => { }); }); + it('should throw any errors that do not match ACTIVATION_REQUIRED', () => { + mockConnection.ping.onFirstCall().rejects(new Error('something something ACTIVATION NOT REQUIRED')); + mockConnection.ping.onSecondCall().resolves(Buffer.from(JSON.stringify({ + version: version + }))); + mockConnection.invokeChainCode.withArgs(mockSecurityContext, 'activateIdentity', []).resolves(); + businessNetworkConnection.connection = mockConnection; + return businessNetworkConnection.ping() + .should.be.rejectedWith(/ACTIVATION NOT REQUIRED/); + }); + + it('should activate the identity if the ping returns ACTIVATION_REQUIRED', () => { + mockConnection.ping.onFirstCall().rejects(new Error('something something ACTIVATION_REQUIRED')); + mockConnection.ping.onSecondCall().resolves(Buffer.from(JSON.stringify({ + version: version + }))); + mockConnection.invokeChainCode.withArgs(mockSecurityContext, 'activateIdentity', []).resolves(); + businessNetworkConnection.connection = mockConnection; + return businessNetworkConnection.ping() + .then(() => { + sinon.assert.calledTwice(mockConnection.ping); + sinon.assert.calledOnce(mockConnection.invokeChainCode); + sinon.assert.calledWith(mockConnection.invokeChainCode, mockSecurityContext, 'activateIdentity', []); + }); + }); + + }); + + describe('#pingInner', () => { + + it('should perform a security check', () => { + sandbox.stub(Util, 'securityCheck'); + mockConnection.ping.resolves(Buffer.from(JSON.stringify({ + version: version + }))); + businessNetworkConnection.connection = mockConnection; + return businessNetworkConnection.pingInner() + .then(() => { + sinon.assert.calledOnce(Util.securityCheck); + }); + }); + + it('should ping the connection', () => { + mockConnection.ping.resolves(Buffer.from(JSON.stringify({ + version: version + }))); + businessNetworkConnection.connection = mockConnection; + return businessNetworkConnection.pingInner() + .then(() => { + sinon.assert.calledOnce(mockConnection.ping); + }); + }); + + }); + + describe('#activate', () => { + + it('should perform a security check', () => { + sandbox.stub(Util, 'securityCheck'); + mockConnection.invokeChainCode.withArgs(mockSecurityContext, 'activateIdentity', []).resolves(); + businessNetworkConnection.connection = mockConnection; + return businessNetworkConnection.activate() + .then(() => { + sinon.assert.calledOnce(Util.securityCheck); + }); + }); + + it('should submit a request to the chaincode for activation', () => { + mockConnection.invokeChainCode.withArgs(mockSecurityContext, 'activateIdentity', []).resolves(); + businessNetworkConnection.connection = mockConnection; + return businessNetworkConnection.activate() + .then(() => { + sinon.assert.calledOnce(mockConnection.invokeChainCode); + sinon.assert.calledWith(mockConnection.invokeChainCode, mockSecurityContext, 'activateIdentity', []); + }); + }); + }); describe('#issueIdentity', () => { @@ -876,12 +954,12 @@ describe('BusinessNetworkConnection', () => { }).should.throw(/participant not specified/); }); - it('should throw if userID not specified', () => { + it('should throw if identityName not specified', () => { (() => { let mockResource = sinon.createStubInstance(Resource); mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1'); businessNetworkConnection.issueIdentity(mockResource, null); - }).should.throw(/userID not specified/); + }).should.throw(/identityName not specified/); }); it('should submit a request to the chaincode for a resource', () => { @@ -893,7 +971,7 @@ describe('BusinessNetworkConnection', () => { sinon.assert.calledOnce(mockConnection.createIdentity); sinon.assert.calledWith(mockConnection.createIdentity, mockSecurityContext, 'dogeid1'); sinon.assert.calledOnce(Util.invokeChainCode); - sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'addParticipantIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']); + sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'issueIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']); result.should.deep.equal({ userID: 'dogeid1', userSecret: 'suchsecret' @@ -908,7 +986,7 @@ describe('BusinessNetworkConnection', () => { sinon.assert.calledOnce(mockConnection.createIdentity); sinon.assert.calledWith(mockConnection.createIdentity, mockSecurityContext, 'dogeid1'); sinon.assert.calledOnce(Util.invokeChainCode); - sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'addParticipantIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']); + sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'issueIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']); result.should.deep.equal({ userID: 'dogeid1', userSecret: 'suchsecret' @@ -923,7 +1001,7 @@ describe('BusinessNetworkConnection', () => { sinon.assert.calledOnce(mockConnection.createIdentity); sinon.assert.calledWith(mockConnection.createIdentity, mockSecurityContext, 'dogeid1', { issuer: true }); sinon.assert.calledOnce(Util.invokeChainCode); - sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'addParticipantIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']); + sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'issueIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']); result.should.deep.equal({ userID: 'dogeid1', userSecret: 'suchsecret' @@ -933,14 +1011,58 @@ describe('BusinessNetworkConnection', () => { }); + describe('#bindIdentity', () => { + + const pem = '-----BEGIN CERTIFICATE-----\nMIIB8jCCAZmgAwIBAgIULKt4c4xcdMwGgjNef9IL92HQkyAwCgYIKoZIzj0EAwIw\nczELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh\nbiBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT\nE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNzA4MTg1NzAwWhcNMTgwNzA4MTg1\nNzAwWjASMRAwDgYDVQQDEwdib29iaWVzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD\nQgAE5P4RNqfEy8pArDxAbVIjRxqkwlpHUY7ANR6X7a4uvVIzIPDx4p7lf37xuc+5\nI9VZCvcI1SA5nIRphet0yYSgZaNsMGowDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwEB\n/wQCMAAwHQYDVR0OBBYEFAmjJfUZvdB8pHvklsdd1HiVog+VMCsGA1UdIwQkMCKA\nIBmrZau7BIB9rRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0cA\nMEQCIGtqR9rUR2ESu2UfUpNUfEeeBsshMkMHmuP/r5uvo2fSAiBtFB9Aid/3nexB\nI5qkVbdRSRQpt7uxoKFDLV/LUDM9xw==\n-----END CERTIFICATE-----\n'; + + beforeEach(() => { + businessNetworkConnection.connection = mockConnection; + }); + + it('should throw if participant not specified', () => { + (() => { + businessNetworkConnection.bindIdentity(null, pem); + }).should.throw(/participant not specified/); + }); + + it('should throw if certificate not specified', () => { + (() => { + let mockResource = sinon.createStubInstance(Resource); + mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1'); + businessNetworkConnection.bindIdentity(mockResource, null); + }).should.throw(/certificate not specified/); + }); + + it('should submit a request to the chaincode for a resource', () => { + sandbox.stub(Util, 'invokeChainCode').resolves(); + let mockResource = sinon.createStubInstance(Resource); + mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1'); + return businessNetworkConnection.bindIdentity(mockResource, pem) + .then(() => { + sinon.assert.calledOnce(Util.invokeChainCode); + sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', pem]); + }); + }); + + it('should submit a request to the chaincode for a fully qualified identifier', () => { + sandbox.stub(Util, 'invokeChainCode').resolves(); + return businessNetworkConnection.bindIdentity('org.doge.Doge#DOGE_1', pem) + .then(() => { + sinon.assert.calledOnce(Util.invokeChainCode); + sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', pem]); + }); + }); + + }); + describe('#revokeIdentity', () => { - it('should throw if identity not specified', () => { + it('should throw if identityId not specified', () => { (() => { let mockResource = sinon.createStubInstance(Resource); mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1'); businessNetworkConnection.revokeIdentity(null); - }).should.throw(/identity not specified/); + }).should.throw(/identityId not specified/); }); it('should submit a request to the chaincode', () => { @@ -948,7 +1070,7 @@ describe('BusinessNetworkConnection', () => { return businessNetworkConnection.revokeIdentity('dogeid1') .then(() => { sinon.assert.calledOnce(Util.invokeChainCode); - sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'removeIdentity', ['dogeid1']); + sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'revokeIdentity', ['dogeid1']); }); }); diff --git a/packages/composer-common/lib/modelmanager.js b/packages/composer-common/lib/modelmanager.js index e8bfcf31be..d975bd9d89 100644 --- a/packages/composer-common/lib/modelmanager.js +++ b/packages/composer-common/lib/modelmanager.js @@ -39,6 +39,44 @@ const SYSTEM_MODEL_CONTENTS = ` o String eventId o DateTime timestamp } + + enum IdentityState { + o ISSUED + o BOUND + o ACTIVATED + o REVOKED + } + + asset Identity identified by identifier extends Asset { + o String identifier + o String name + o String issuer + o String certificate + o IdentityState state + --> Participant participant + } + + abstract transaction IdentityTransaction extends Transaction { + + } + + transaction IssueIdentity extends IdentityTransaction { + o String name + --> Participant participant + } + + transaction BindIdentity extends IdentityTransaction { + o String identifier + --> Participant participant + } + + transaction ActivateIdentity extends IdentityTransaction { + + } + + transaction RevokeIdentity extends IdentityTransaction { + o String identifier + } `; /** diff --git a/packages/composer-runtime-hlfv1/identityservice.go b/packages/composer-runtime-hlfv1/identityservice.go index 59a8af20cc..4d6c34fbde 100644 --- a/packages/composer-runtime-hlfv1/identityservice.go +++ b/packages/composer-runtime-hlfv1/identityservice.go @@ -16,8 +16,9 @@ package main import ( "bytes" - "strings" + "crypto/sha256" "crypto/x509" + "encoding/hex" "encoding/pem" "errors" @@ -28,8 +29,14 @@ import ( // IdentityService is a Go wrapper around an instance of the IdentityService JavaScript class. type IdentityService struct { - VM *duktape.Context - Stub shim.ChaincodeStubInterface + VM *duktape.Context + Stub shim.ChaincodeStubInterface + Certificate *x509.Certificate + CurrentUserID string + Identifier string + Name string + Issuer string + PEM string } // NewIdentityService creates a Go wrapper around a new instance of the IdentityService JavaScript class. @@ -43,11 +50,17 @@ func NewIdentityService(vm *duktape.Context, context *Context, stub shim.Chainco // Create the new identity service. result = &IdentityService{VM: vm, Stub: stub} + // Load the certificate. + err := result.loadCertificate() + if err != nil { + panic(err) + } + // Create a new instance of the JavaScript IdentityService class. vm.PushGlobalObject() // [ global ] vm.GetPropString(-1, "composer") // [ global composer ] vm.GetPropString(-1, "IdentityService") // [ global composer IdentityService ] - err := vm.Pnew(0) // [ global composer theIdentityService ] + err = vm.Pnew(0) // [ global composer theIdentityService ] if err != nil { panic(err) } @@ -59,65 +72,101 @@ func NewIdentityService(vm *duktape.Context, context *Context, stub shim.Chainco vm.Pop() // [ global composer theIdentityService ] // Bind the methods into the JavaScript object. - vm.PushGoFunction(result.getCurrentUserID) // [ global composer theIdentityService getCurrentUserID ] - vm.PutPropString(-2, "getCurrentUserID") // [ global composer theIdentityService ] + vm.PushGoFunction(result.getIdentifier) // [ global composer theIdentityService getIdentifier ] + vm.PutPropString(-2, "getIdentifier") // [ global composer theIdentityService ] + vm.PushGoFunction(result.getName) // [ global composer theIdentityService getName ] + vm.PutPropString(-2, "getName") // [ global composer theIdentityService ] + vm.PushGoFunction(result.getIssuer) // [ global composer theIdentityService getIssuer ] + vm.PutPropString(-2, "getIssuer") // [ global composer theIdentityService ] + vm.PushGoFunction(result.getCertificate) // [ global composer theIdentityService getCertificate ] + vm.PutPropString(-2, "getCertificate") // [ global composer theIdentityService ] // Return the new identity service. return result } -// extract the common name from the creator x509 certificate +// load the creator x509 certificate // Although we should really use protobufs to correctly locate the certificate // this would have meant pulling in a load of vendor dependencies as they aren't // in the chaincode container and it isn't worth it. -func extractNameFromCreator(stub shim.ChaincodeStubInterface) (result string, errResp error) { - logger.Debug("Entering extractNameFromCreator", &stub) - defer func() {logger.Debug("Exiting extractNameFromCreator", result, errResp)}() - creator, err := stub.GetCreator() +func (identityService *IdentityService) loadCertificate() (errResp error) { + logger.Debug("Entering loadCertificate") + defer func() { logger.Debug("Exiting loadCertificate", errResp) }() + + creator, err := identityService.Stub.GetCreator() if err != nil { - return "", err + return err } logger.Debug("creator", string(creator)) - certStart := bytes.Index(creator,[]byte("-----BEGIN CERTIFICATE-----")) + certStart := bytes.Index(creator, []byte("-----BEGIN CERTIFICATE-----")) if certStart == -1 { - return "", errors.New("No Certificate found") + return errors.New("No Certificate found") } certText := creator[certStart:] block, _ := pem.Decode(certText) if block == nil { - return "", errors.New("Error received on pem.Decode of certificate:" + string(certText)) + return errors.New("Error received on pem.Decode of certificate:" + string(certText)) } ucert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return "", err + return err } + identityService.Certificate = ucert + identityService.PEM = string(certText) + return nil +} + +// getIdentifier gets a unique identifier for the identity used to submit the transaction. +func (identityService *IdentityService) getIdentifier(vm *duktape.Context) (result int) { + logger.Debug("Entering IdentityService.getIdentifier", vm) + defer func() { logger.Debug("Exiting IdentityService.getIdentifier", result) }() + + // Create a fingerprint of the certificate. + bytes := identityService.Certificate.Raw + hash := sha256.New() + hash.Write(bytes) + fingerprint := hex.EncodeToString(hash.Sum(nil)) - return ucert.Subject.CommonName, nil + // Return the fingerprint. + vm.PushString(fingerprint) + return 1 } -// getCurrentUserID retrieves the userID attribute from the users certificate. -func (identityService *IdentityService) getCurrentUserID(vm *duktape.Context) (result int) { - logger.Debug("Entering IdentityService.getCurrentUserID", vm) - defer func() { logger.Debug("Exiting IdentityService.getCurrentUserID", result) }() +// getName gets the name of the identity used to submit the transaction. +func (identityService *IdentityService) getName(vm *duktape.Context) (result int) { + logger.Debug("Entering IdentityService.getName", vm) + defer func() { logger.Debug("Exiting IdentityService.getName", result) }() - creatorName, err := extractNameFromCreator(identityService.Stub) - if err != nil { - vm.PushErrorObjectVa(duktape.ErrError, "%s", err.Error()) - vm.Throw() - return 0 - } + // Return the common name of the certificate. + name := identityService.Certificate.Subject.CommonName + vm.PushString(name) + return 1 +} - logger.Debug("Common Name", creatorName) +// getIssuer gets the issuer of the identity used to submit the transaction. +func (identityService *IdentityService) getIssuer(vm *duktape.Context) (result int) { + logger.Debug("Entering IdentityService.getIssuer", vm) + defer func() { logger.Debug("Exiting IdentityService.getIssuer", result) }() - // TODO: Will be upgraded to a new security model soon. - // returning Null grants any common name with the word admin in - // it to have all authority - if strings.Contains(strings.ToLower(creatorName), "admin") { - vm.PushNull() - return 1 - } - vm.PushString(creatorName) + // Create a fingerprint of the issuer of the certificate. + bytes := identityService.Certificate.RawIssuer + hash := sha256.New() + hash.Write(bytes) + fingerprint := hex.EncodeToString(hash.Sum(nil)) + + // Return the fingerprint. + vm.PushString(fingerprint) + return 1 +} + +// getCertificate gets the certificate used to submit the transaction. +func (identityService *IdentityService) getCertificate(vm *duktape.Context) (result int) { + logger.Debug("Entering IdentityService.getCertificate", vm) + defer func() { logger.Debug("Exiting IdentityService.getCertificate", result) }() + + // Return the certificate. + vm.PushString(identityService.PEM) return 1 } diff --git a/packages/composer-runtime/lib/compiledscriptbundle.js b/packages/composer-runtime/lib/compiledscriptbundle.js index dc71e16950..79e291fa87 100644 --- a/packages/composer-runtime/lib/compiledscriptbundle.js +++ b/packages/composer-runtime/lib/compiledscriptbundle.js @@ -51,8 +51,9 @@ class CompiledScriptBundle { // If we didn't find any functions to call, then throw an error! if (functionNames.length === 0) { - LOG.error(`Could not find any functions to execute for transaction ${resolvedTransaction.getFullyQualifiedIdentifier()}`); - throw new Error(`Could not find any functions to execute for transaction ${resolvedTransaction.getFullyQualifiedIdentifier()}`); + const error = new Error(`Could not find any functions to execute for transaction ${resolvedTransaction.getFullyQualifiedIdentifier()}`); + LOG.error(method, error); + throw error; } // Generate an instance of the compiled script bundle. diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js index 88396ae600..3e8b264432 100644 --- a/packages/composer-runtime/lib/context.js +++ b/packages/composer-runtime/lib/context.js @@ -97,17 +97,19 @@ class Context { */ constructor(engine) { this.engine = engine; + this.function = null; + this.arguments = null; this.businessNetworkDefinition = null; this.registryManager = null; this.resolver = null; this.api = null; this.queryExecutor = null; this.identityManager = null; + this.identity = null; this.participant = null; this.transaction = null; this.accessController = null; this.sysregistries = null; - this.sysidentities = null; this.eventNumber = 0; this.scriptCompiler = null; this.compiledScriptBundle = null; @@ -117,6 +119,22 @@ class Context { this.compiledAclBundle = null; } + /** + * Get the name of the currently executing runtime method. + * @return {string} The name of the currently executing runtime method. + */ + getFunction() { + return this.function; + } + + /** + * Get the arguments for the currently executing runtime method. + * @return {string} The arguments for the currently executing runtime method. + */ + getArguments() { + return this.arguments; + } + /** * Load the business network record from the world state. * @return {Promise} A promise that will be resolved with the business network record @@ -135,7 +153,7 @@ class Context { if (object.undeployed){ throw new Error('The business network has been undeployed'); } - LOG.exit(object); + LOG.exit(method, object); return object; }); } @@ -267,26 +285,52 @@ class Context { loadCurrentParticipant() { const method = 'loadCurrentParticipant'; LOG.entry(method); - let currentUserID = this.getIdentityService().getCurrentUserID(); - LOG.debug(method, 'Got current user ID', currentUserID); - if (currentUserID) { - return this.getIdentityManager().getParticipant(currentUserID) - .then((participant) => { - LOG.debug(method, 'Found current participant', participant.getFullyQualifiedIdentifier()); - LOG.exit(method, participant); - return participant; - }) - .catch((error) => { - LOG.error(method, 'Could not find current participant', error); - throw new Error(`Could not determine the participant for identity '${currentUserID}'. The identity may be invalid or may have been revoked.`); - }); - } else { - // TODO: this is temporary whilst we migrate to requiring all - // users to have identities that are mapped to participants. - LOG.debug(method, 'Could not determine current user ID'); - LOG.exit(method, null); - return Promise.resolve(null); - } + + // Load the current identity. + return this.getIdentityManager().getIdentity() + .then((identity) => { + + // Save the current identity. + this.identity = identity; + + // Validate the identity. + try { + this.getIdentityManager().validateIdentity(identity); + } catch (e) { + + // Check for the case of activation required, and the user is trying to activate. + if (e.activationRequired && this.getFunction() === 'activateIdentity') { + // Ignore. + } else { + throw e; + } + + } + + // Load the current participant. + return this.getIdentityManager().getParticipant(identity); + + }) + .then((participant) => { + LOG.exit(method, participant); + return participant; + }) + .catch((error) => { + + // Check for an admin user. + // TODO: this is temporary whilst we migrate to requiring all + // users to have identities that are mapped to participants. + const name = this.getIdentityService().getName(); + if (name && name.match(/admin/i)) { + LOG.exit(method, null); + return null; + } + + // Throw the error. + LOG.error(method, error); + throw error; + + }); } /** @@ -415,12 +459,13 @@ class Context { /** * Initialize the context for use. * @param {Object} [options] The options to use. + * @param {string} [options.function] The name of the currently executing runtime method. + * @param {string} [options.arguments] The arguments for the currently executing runtime method. * @param {BusinessNetworkDefinition} [options.businessNetworkDefinition] The business network definition to use. * @param {CompiledScriptBundle} [options.compiledScriptBundle] The compiled script bundle to use. + * @param {DataCollection} [options.sysregistries] The system registries collection to use. * @param {boolean} [options.reinitialize] Set to true if being reinitialized as a result of an upgrade to the * business network, falsey value if not. - * @param {DataCollection} [options.sysregistries] The system registries collection to use. - * @param {DataCollection} [options.sysidentities] The system identities collection to use. * @return {Promise} A promise that will be resolved when complete, or rejected * with an error. */ @@ -428,6 +473,8 @@ class Context { const method = 'initialize'; LOG.entry(method, options); options = options || {}; + this.function = options.function || this.function; + this.arguments = options.arguments || this.arguments; return Promise.resolve() .then(() => { return this.findBusinessNetworkDefinition(options); @@ -460,17 +507,6 @@ class Context { }); } }) - .then(() => { - LOG.debug(method, 'Loading sysidentities collection', options.sysidentities); - if (options.sysidentities) { - this.sysidentities = options.sysidentities; - } else { - return this.getDataService().getCollection('$sysidentities') - .then((sysidentities) => { - this.sysidentities = sysidentities; - }); - } - }) .then(() => { LOG.debug(method, 'Loading current participant'); return this.loadCurrentParticipant(); @@ -654,7 +690,7 @@ class Context { */ getIdentityManager() { if (!this.identityManager) { - this.identityManager = new IdentityManager(this.getDataService(), this.getRegistryManager(), this.getSystemIdentities()); + this.identityManager = new IdentityManager(this); } return this.identityManager; } @@ -722,17 +758,6 @@ class Context { return this.sysregistries; } - /** - * Get the system identities collection. - * @return {DataCollection} The system registries collection. - */ - getSystemIdentities() { - if (!this.sysidentities) { - throw new Error('must call initialize before calling this function'); - } - return this.sysidentities; - } - /** * Get the next event number * @return {integer} the event number. diff --git a/packages/composer-runtime/lib/engine.identities.js b/packages/composer-runtime/lib/engine.identities.js index 2b24dfecb3..a4479a05e8 100644 --- a/packages/composer-runtime/lib/engine.identities.js +++ b/packages/composer-runtime/lib/engine.identities.js @@ -27,46 +27,89 @@ const LOG = Logger.getLog('EngineIdentities'); class EngineIdentities { /** - * Add a mapping from the specified identity, or user ID, to the specified - * participant. + * Issue a new identity to a participant in the business network. * @param {Context} context The request context. * @param {string[]} args The arguments to pass to the chaincode function. * @return {Promise} A promise that will be resolved when complete, or rejected * with an error. */ - addParticipantIdentity(context, args) { - const method = 'addParticipantIdentity'; + issueIdentity(context, args) { + const method = 'issueIdentity'; LOG.entry(method, context, args); if (args.length !== 2) { LOG.error(method, 'Invalid arguments', args); - throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'addParticipantIdentity', ['participantId', 'userId'])); + throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'issueIdentity', ['participantFQI', 'identityName'])); } - let participantId = args[0]; - let userId = args[1]; + let participantFQI = args[0]; + let identityName = args[1]; let identityManager = context.getIdentityManager(); - return identityManager.addIdentityMapping(participantId, userId) + return identityManager.issueIdentity(participantFQI, identityName) .then(() => { LOG.exit(method); }); } /** - * Remove any mapping for the specified identity, or user ID. + * Bind an existing identity to a participant in the business network. * @param {Context} context The request context. * @param {string[]} args The arguments to pass to the chaincode function. * @return {Promise} A promise that will be resolved when complete, or rejected * with an error. */ - removeIdentity(context, args) { - const method = 'removeIdentity'; + bindIdentity(context, args) { + const method = 'bindIdentity'; + LOG.entry(method, context, args); + if (args.length !== 2) { + LOG.error(method, 'Invalid arguments', args); + throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'bindIdentity', ['participantFQI', 'certificate'])); + } + let participantFQI = args[0]; + let certificate = args[1]; + let identityManager = context.getIdentityManager(); + return identityManager.bindIdentity(participantFQI, certificate) + .then(() => { + LOG.exit(method); + }); + } + + /** + * Activate the current identity in the business network. + * @param {Context} context The request context. + * @param {string[]} args The arguments to pass to the chaincode function. + * @return {Promise} A promise that will be resolved when complete, or rejected + * with an error. + */ + activateIdentity(context, args) { + const method = 'activateIdentity'; + LOG.entry(method, context, args); + if (args.length !== 0) { + LOG.error(method, 'Invalid arguments', args); + throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'activateIdentity', [])); + } + let identityManager = context.getIdentityManager(); + return identityManager.activateIdentity() + .then(() => { + LOG.exit(method); + }); + } + + /** + * Revoke an identity in the business network. + * @param {Context} context The request context. + * @param {string[]} args The arguments to pass to the chaincode function. + * @return {Promise} A promise that will be resolved when complete, or rejected + * with an error. + */ + revokeIdentity(context, args) { + const method = 'revokeIdentity'; LOG.entry(method, context, args); if (args.length !== 1) { LOG.error(method, 'Invalid arguments', args); - throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'removeIdentity', ['userId'])); + throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'revokeIdentity', ['identityId'])); } - let userId = args[0]; + let identityId = args[0]; let identityManager = context.getIdentityManager(); - return identityManager.removeIdentityMapping(userId) + return identityManager.revokeIdentity(identityId) .then(() => { LOG.exit(method); }); diff --git a/packages/composer-runtime/lib/engine.js b/packages/composer-runtime/lib/engine.js index 9cc9d37725..fed4d950ef 100644 --- a/packages/composer-runtime/lib/engine.js +++ b/packages/composer-runtime/lib/engine.js @@ -105,7 +105,7 @@ class Engine { let dataService = context.getDataService(); let businessNetworkBase64, businessNetworkHash, businessNetworkRecord, businessNetworkDefinition; let compiledScriptBundle, compiledQueryBundle, compiledAclBundle; - let sysregistries, sysidentities; + let sysregistries; return Promise.resolve() .then(() => { @@ -175,28 +175,19 @@ class Engine { sysregistries = sysregistries_; }); - }) - .then(() => { - - // Ensure that the system identities collection exists. - LOG.debug(method, 'Ensuring that sysidentities collection exists'); - return dataService.ensureCollection('$sysidentities') - .then((sysidentities_) => { - sysidentities = sysidentities_; - }); - }) .then(() => { // Initialize the context. LOG.debug(method, 'Initializing context'); return context.initialize({ + function: fcn, + arguments: args, businessNetworkDefinition: businessNetworkDefinition, compiledScriptBundle: compiledScriptBundle, compiledQueryBundle: compiledQueryBundle, compiledAclBundle: compiledAclBundle, - sysregistries: sysregistries, - sysidentities: sysidentities + sysregistries: sysregistries }); }) @@ -270,7 +261,7 @@ class Engine { LOG.entry(method, context, fcn, args); if (this[fcn]) { LOG.debug(method, 'Initializing context'); - return context.initialize() + return context.initialize({ function: fcn, arguments: args }) .then(() => { return context.transactionStart(false); }) @@ -341,7 +332,7 @@ class Engine { LOG.entry(method, context, fcn, args); if (this[fcn]) { LOG.debug(method, 'Initializing context'); - return context.initialize() + return context.initialize({ function: fcn, arguments: args }) .then(() => { return context.transactionStart(true); }) diff --git a/packages/composer-runtime/lib/engine.logging.js b/packages/composer-runtime/lib/engine.logging.js index 5ad4b698db..34f54b4a29 100644 --- a/packages/composer-runtime/lib/engine.logging.js +++ b/packages/composer-runtime/lib/engine.logging.js @@ -63,7 +63,7 @@ class EngineLogging { } if (context.getParticipant() === null) { const curLogLevel = this.getContainer().getLoggingService().getLogLevel(); - LOG.debug('current log level=' + curLogLevel); + LOG.debug(method, 'current log level=' + curLogLevel); return Promise.resolve(curLogLevel); } throw new Error('Authorization failure'); diff --git a/packages/composer-runtime/lib/httpservice.js b/packages/composer-runtime/lib/httpservice.js index 77097e9e8a..64d3d2bb8c 100644 --- a/packages/composer-runtime/lib/httpservice.js +++ b/packages/composer-runtime/lib/httpservice.js @@ -53,7 +53,7 @@ class HTTPService extends Service { else { response = responseThing; } - LOG.info('Reponse from URL ' + url, JSON.stringify(response)); + LOG.info(method, 'Reponse from URL ' + url, JSON.stringify(response)); if(response.statusCode >= 200 && response.statusCode < 300) { if(response.body && typeof response.body === 'string') { @@ -61,13 +61,13 @@ class HTTPService extends Service { response.body = JSON.parse(response.body); } catch(err) { - LOG.warn('Body data could not be converted to JS object', response.body); + LOG.warn(method, 'Body data could not be converted to JS object', response.body); } } return Promise.resolve(response); } else { - LOG.error('Error statusCode ', response.statusCode); + LOG.error(method, 'Error statusCode ', response.statusCode); return Promise.reject(JSON.stringify(response)); } }) diff --git a/packages/composer-runtime/lib/identitymanager.js b/packages/composer-runtime/lib/identitymanager.js index 2aae9ee7f2..fb3eb4ce94 100644 --- a/packages/composer-runtime/lib/identitymanager.js +++ b/packages/composer-runtime/lib/identitymanager.js @@ -14,8 +14,9 @@ 'use strict'; +const createHash = require('sha.js'); const Logger = require('composer-common').Logger; -const Resource = require('composer-common').Resource; +const ModelUtil = require('composer-common').ModelUtil; const LOG = Logger.getLog('IdentityManager'); @@ -27,61 +28,241 @@ class IdentityManager { /** * Constructor. - * @param {DataService} dataService The data service to use. - * @param {RegistryManager} registryManager The registry manager to use. - * @param {DataCollection} sysidentities The system identities collection. + * @param {Context} context The request context. */ - constructor(dataService, registryManager, sysidentities) { - this.dataService = dataService; - this.registryManager = registryManager; - this.sysidentities = sysidentities; + constructor(context) { + this.identityService = context.getIdentityService(); + this.registryManager = context.getRegistryManager(); + this.factory = context.getFactory(); } /** - * Add a new mapping for the specified identity (user ID) to the specified - * participant. - * @param {(Resource|string)} participant The participant, or the unique - * identifier of the participant. - * @param {string} userID The identity (user ID) to map to the participant. - * @return {Promise} A promise that is resolved when a new mapping for the - * specified identity has been created. + * Get the identity registry. + * @return {Promise} A promise that will be resolved with an {@link Registry} + * when complete, or rejected with an error. */ - addIdentityMapping(participant, userID) { - const method = 'addIdentityMapping'; - LOG.entry(method, participant, userID); - let participantFQI, participantFQT, participantID; - if (participant instanceof Resource) { - participantFQI = participant.getFullyQualifiedIdentifier(); - participantFQT = participant.getFullyQualifiedType(); - participantID = participant.getIdentifier(); - } else { - participantFQI = participant; - let hashIndex = participantFQI.indexOf('#'); - if (hashIndex === -1) { - throw new Error('Invalid fully qualified participant identifier'); - } - participantFQT = participantFQI.substring(0, hashIndex); - participantID = participantFQI.substring(hashIndex + 1); + getIdentityRegistry() { + const method = 'getIdentityRegistry'; + LOG.entry(method); + return this.registryManager.get('Asset', 'org.hyperledger.composer.system.Identity') + .then((identityRegistry) => { + LOG.exit(method, identityRegistry); + return identityRegistry; + }); + } + + /** + * Find the identity in the identity registry that maps to the certificate that + * was used to sign and submit the current transaction. + * @return {Promise} A promise that will be resolved with a {@link Resource} + * when complete, or rejected with an error. + */ + getIdentity() { + const method = 'getIdentity'; + LOG.entry(method); + let identityRegistry, identifier; + return this.getIdentityRegistry() + .then((identityRegistry_) => { + + // Check to see if the identity exists. + identityRegistry = identityRegistry_; + identifier = this.identityService.getIdentifier(); + return identityRegistry.exists(identifier); + + }) + .then((exists) => { + + // If it doesn't exist, then try again with the temporary identifier, which is hash(name, issuer). + if (!exists) { + const sha256 = createHash('sha256'); + const name = this.identityService.getName(); + const issuer = this.identityService.getIssuer(); + sha256.update(name, 'utf8'); + sha256.update(issuer, 'utf8'); + identifier = sha256.digest('hex'); + return identityRegistry.exists(identifier); + } else { + return exists; + } + + }) + .then((exists) => { + + // If it still doesn't exist, throw! + if (!exists) { + const error = new Error('The current identity has not been registered'); + LOG.error(method, error); + throw error; + } + + // Get the identity. + return identityRegistry.get(identifier); + + }) + .then((identity) => { + LOG.exit(method, identity); + return identity; + }); + } + + /** + * Validate the specified identity to confirm that it is valid for use with the + * business network. We validate that the identity is not revoked, pending activation, + * or in an invalid state. + * @param {Resource} identity The identity to validate. + */ + validateIdentity(identity) { + const method = 'validateIdentity'; + LOG.entry(method, identity); + + // Check for a revoked identity. + if (identity.state === 'REVOKED') { + const error = new Error('The current identity has been revoked'); + LOG.error(method, error); + throw error; } - LOG.debug(method, 'Looking for participant registry', participantFQT); + + // Check for an issued or bound identity, in which case activation is required. + if (identity.state === 'ISSUED' || identity.state === 'BOUND') { + const error = new Error('The current identity must be activated (ACTIVATION_REQUIRED)'); + error.activationRequired = true; + LOG.error(method, error); + throw error; + } + + // Ensure that the identity is activated. + if (identity.state !== 'ACTIVATED') { + const error = new Error('The current identity is in an unknown state: ' + identity.state); + LOG.error(method, error); + throw error; + } + + LOG.exit(method); + } + + /** + * Find the participant for the specified identity. + * @param {Resource} identity The identity to find the participant for. + * @return {Promise} A promise that will be resolved with a {@link Resource} + * when complete, or rejected with an error. + */ + getParticipant(identity) { + const method = 'getParticipant'; + LOG.entry(method); + const participant = identity.participant; + const participantFQT = participant.getFullyQualifiedType(); return this.registryManager.get('Participant', participantFQT) .then((participantRegistry) => { - LOG.debug(method, 'Found participant registry, looking for participant', participantID); - return participantRegistry.get(participantID); + return participantRegistry.get(participant.getIdentifier()); }) .then((participant) => { - LOG.debug(method, 'Got $sysidentities collection, checking for existing mapping'); - return this.sysidentities.exists(userID); + LOG.exit(method, participant); + return participant; }) - .then((exists) => { - if (exists) { - LOG.error(method, 'Found an existing mapping for user ID', userID); - throw new Error(`Found an existing mapping for user ID '${userID}'`); - } - LOG.debug(method, 'No existing mapping exists for user ID, adding'); - return this.sysidentities.add(userID, { - participant: participantFQI + .catch(() => { + const error = new Error('The current identity is bound to a participant that does not exist'); + LOG.error(method, error); + throw error; + }); + } + + /** + * Parse the fully qualified participant identifier into a relationship to that participant. + * @param {string} participantFQI The fully qualified participant identifier. + * @return {Relationship} A relationship to the specified participant. + */ + parseParticipant(participantFQI) { + const method = 'parseParticipant'; + LOG.entry(method, participantFQI); + const hashIndex = participantFQI.indexOf('#'); + if (hashIndex === -1) { + throw new Error('Invalid fully qualified participant identifier'); + } + const participantFQT = participantFQI.substring(0, hashIndex); + const participantNS = ModelUtil.getNamespace(participantFQT); + const participantType = ModelUtil.getShortName(participantFQT); + const participantID = participantFQI.substring(hashIndex + 1); + const participant = this.factory.newRelationship(participantNS, participantType, participantID); + LOG.exit(method, participant); + return participant; + } + + /** + * Issue a new identity to a participant in the business network. + * @param {string} participantFQI The fully qualified participant identifier. + * @param {string} identityName The name of the new identity. + * @return {Promise} A promise that will be resolved when complete, or rejected + * with an error. + */ + issueIdentity(participantFQI, identityName) { + const method = 'issueIdentity'; + LOG.entry(method, participantFQI, identityName); + return this.getIdentityRegistry() + .then((identityRegistry) => { + + // Create the temporary identifier, which is hash(name, issuer) + const sha256 = createHash('sha256'); + const issuer = this.identityService.getIssuer(); + sha256.update(identityName, 'utf8'); + sha256.update(issuer, 'utf8'); + const identifier = sha256.digest('hex'); + + // Parse the participant into a relationship. + const participant = this.parseParticipant(participantFQI); + + // Create the new identity and add it to the identity registry. + const identity = this.factory.newResource('org.hyperledger.composer.system', 'Identity', identifier); + Object.assign(identity, { + name: identityName, + issuer, + certificate: '', + state: 'ISSUED', + participant + }); + return identityRegistry.add(identity); + + }) + .then(() => { + LOG.exit(method); + }); + } + + /** + * Bind an existing identity to a participant in the business network. + * @param {string} participantFQI The fully qualified participant identifier. + * @param {string} certificate The certificate for the existing identity. + * @return {Promise} A promise that will be resolved when complete, or rejected + * with an error. + */ + bindIdentity(participantFQI, certificate) { + const method = 'bindIdentity'; + LOG.entry(method, participantFQI, certificate); + return this.getIdentityRegistry() + .then((identityRegistry) => { + + // Parse the certificate into a byte array. + const bytes = certificate + .replace(/-----BEGIN CERTIFICATE-----/, '') + .replace(/-----END CERTIFICATE-----/, '') + .replace(/[\r\n]+/g, ''); + const buffer = Buffer.from(bytes, 'base64'); + const sha256 = createHash('sha256'); + const identityId = sha256.update(buffer).digest('hex'); + + // Parse the participant into a relationship. + const participant = this.parseParticipant(participantFQI); + + // Create the new identity and add it to the identity registry. + const identity = this.factory.newResource('org.hyperledger.composer.system', 'Identity', identityId); + Object.assign(identity, { + name: '', + issuer: '', + certificate, + state: 'BOUND', + participant }); + return identityRegistry.add(identity); + }) .then(() => { LOG.exit(method); @@ -89,23 +270,34 @@ class IdentityManager { } /** - * Remove an existing mapping for the specified identity (user ID) to a - * participant. - * @param {string} userID The identity (user ID). - * @return {Promise} A promise that is resolved when a new mapping for the - * specified identity has been created. + * Activate the current identity in the business network. + * @return {Promise} A promise that will be resolved when complete, or rejected + * with an error. */ - removeIdentityMapping(userID) { - const method = 'removeIdentityMapping'; - LOG.entry(method, userID); - LOG.debug(method, 'Got $sysidentities collection, checking for existing mapping'); - return this.sysidentities.exists(userID) - .then((exists) => { - if (!exists) { - LOG.debug('No existing mapping exists for user ID, ignoring'); - return; + activateIdentity() { + const method = 'activateIdentity'; + LOG.entry(method); + let identityRegistry; + return this.getIdentityRegistry() + .then((identityRegistry_) => { + identityRegistry = identityRegistry_; + return this.getIdentity(); + }) + .then((identity) => { + + // If the identity has been issued, we must delete it and then create a new one. + if (identity.state === 'ISSUED') { + return this.activateIssuedIdentity(identityRegistry, identity); } - return this.sysidentities.remove(userID); + + // If the identity has been bound, then we can update it. + if (identity.state === 'BOUND') { + return this.activateBoundIdentity(identityRegistry, identity); + } + + // Shouldn't get here. + throw new Error('The current identity cannot be activated because it is in an unknown state: ' + identity.state); + }) .then(() => { LOG.exit(method); @@ -113,37 +305,100 @@ class IdentityManager { } /** - * Retrieve the participant for the specified identity (user ID). - * @param {string} userID The identity (user ID). - * @return {Promise} A promise that is resolved with a {@link Resource} - * representing the participant, or rejected with an error. + * Activate the specified identity (in the ISSUED state) in the business network. + * @param {Registry} identityRegistry The identity registry. + * @param {Resource} identity The identity to activate. + * @return {Promise} A promise that will be resolved when complete, or rejected + * with an error. */ - getParticipant(userID) { - const method = 'getParticipant'; - LOG.entry(method, userID); - LOG.debug(method, 'Getting $sysidentities collection'); - let participantFQI, participantFQT, participantID; - LOG.debug(method, 'Got $sysidentities collection, checking for existing mapping'); - return this.sysidentities.get(userID) - .then((mapping) => { - participantFQI = mapping.participant; - LOG.debug(method, 'Found mapping, participant is', participantFQI); - let hashIndex = participantFQI.indexOf('#'); - if (hashIndex === -1) { - throw new Error('Invalid fully qualified participant identifier'); - } - participantFQT = participantFQI.substring(0, hashIndex); - participantID = participantFQI.substring(hashIndex + 1); - LOG.debug(method, 'Looking for participant registry', participantFQT); - return this.registryManager.get('Participant', participantFQT); + activateIssuedIdentity(identityRegistry, identity) { + const method = 'activateIssuedIdentity'; + LOG.entry(method, identityRegistry, identity); + + // Grab information from the certificate. + const identifier = this.identityService.getIdentifier(); + const name = this.identityService.getName(); + const issuer = this.identityService.getIssuer(); + const certificate = this.identityService.getCertificate(); + + // Validate the issuer to check it matches the issuer of the identity. + if (identity.issuer !== issuer) { + throw new Error('The current identity cannot be activated because the issuer is invalid'); + } + + // Create the new identity. + const newIdentity = this.factory.newResource('org.hyperledger.composer.system', 'Identity', identifier); + Object.assign(newIdentity, { + name, + issuer, + certificate, + state: 'ACTIVATED', + participant: identity.participant + }); + + // Remove the old identity and add the new identity into the identity registry. + return identityRegistry.remove((identity)) + .then(() => { + return identityRegistry.add(newIdentity); }) - .then((participantRegistry) => { - LOG.debug(method, 'Found participant registry, looking for participant', participantID); - return participantRegistry.get(participantID); + .then(() => { + LOG.exit(method); + }); + } + + /** + * Activate the specified identity (in the BOUND state) in the business network. + * @param {Registry} identityRegistry The identity registry. + * @param {Resource} identity The identity to activate. + * @return {Promise} A promise that will be resolved when complete, or rejected + * with an error. + */ + activateBoundIdentity(identityRegistry, identity) { + const method = 'activateBoundIdentity'; + LOG.entry(method, identityRegistry, identity); + + // Grab information from the certificate. + const name = this.identityService.getName(); + const issuer = this.identityService.getIssuer(); + + // Update the identity and update it in the identity registry. + Object.assign(identity, { + name, + issuer, + state: 'ACTIVATED' + }); + return identityRegistry.update(identity) + .then(() => { + LOG.exit(method); + }); + } + + /** + * Revoke an identity in the business network. + * @param {string} identityId The identifier of the identity. + * @return {Promise} A promise that will be resolved when complete, or rejected + * with an error. + */ + revokeIdentity(identityId) { + const method = 'revokeIdentity'; + LOG.entry(method, identityId); + let identityRegistry; + return this.getIdentityRegistry() + .then((identityRegistry_) => { + identityRegistry = identityRegistry_; + return identityRegistry.get(identityId); }) - .then((participant) => { - LOG.exit(method, participant); - return participant; + .then((identity) => { + + // Revoke the identity and updaye it in the identity registry. + Object.assign(identity, { + state: 'REVOKED' + }); + return identityRegistry.update(identity); + + }) + .then(() => { + LOG.exit(method); }); } diff --git a/packages/composer-runtime/lib/identityservice.js b/packages/composer-runtime/lib/identityservice.js index f18d74f6b4..5bc7037f8e 100644 --- a/packages/composer-runtime/lib/identityservice.js +++ b/packages/composer-runtime/lib/identityservice.js @@ -25,12 +25,38 @@ const Service = require('./service'); class IdentityService extends Service { /** - * Retrieve the current user ID. + * Get a unique identifier for the identity used to submit the transaction. * @abstract - * @return {string} The current user ID, or null if the current user ID cannot - * be determined or has not been specified. + * @return {string} A unique identifier for the identity used to submit the transaction. */ - getCurrentUserID() { + getIdentifier() { + throw new Error('abstract function called'); + } + + /** + * Get the name of the identity used to submit the transaction. + * @abstract + * @return {string} The name of the identity used to submit the transaction. + */ + getName() { + throw new Error('abstract function called'); + } + + /** + * Get the issuer of the identity used to submit the transaction. + * @abstract + * @return {string} The issuer of the identity used to submit the transaction. + */ + getIssuer() { + throw new Error('abstract function called'); + } + + /** + * Get the certificate for the identity used to submit the transaction. + * @abstract + * @return {string} The certificate for the identity used to submit the transaction. + */ + getCertificate() { throw new Error('abstract function called'); } diff --git a/packages/composer-runtime/lib/queryexecutor.js b/packages/composer-runtime/lib/queryexecutor.js index 0cf90f9c34..84c5118151 100644 --- a/packages/composer-runtime/lib/queryexecutor.js +++ b/packages/composer-runtime/lib/queryexecutor.js @@ -305,7 +305,7 @@ class QueryExecutor { foundRelationships.forEach((foundRelationship) => { LOG.debug(method, 'Found relationship object', foundRelationship.relationship.toString()); this.modifyRelationship(foundRelationship.relationship, (name) => { - LOG.debug('Relationship accessed', foundRelationship.relationship.toString()); + LOG.debug(method, 'Relationship accessed', foundRelationship.relationship.toString()); return this.resolver.resolveRelationship(foundRelationship.relationship, { cachedResources: new Map(), skipRecursion: true diff --git a/packages/composer-runtime/lib/transactionlogger.js b/packages/composer-runtime/lib/transactionlogger.js index 65c8cb6e44..c5d72360b5 100644 --- a/packages/composer-runtime/lib/transactionlogger.js +++ b/packages/composer-runtime/lib/transactionlogger.js @@ -53,7 +53,7 @@ class TransactionLogger { onResourceAdded(event) { const method = 'onResourceAdded'; LOG.entry(method, event); - LOG.exit(); + LOG.exit(method); } /** @@ -78,7 +78,7 @@ class TransactionLogger { let patches = jsonpatch.compare(oldJSON, newJSON); LOG.debug(method, 'Generated JSON Patch', patches); - LOG.exit(); + LOG.exit(method); } /** @@ -88,7 +88,7 @@ class TransactionLogger { onResourceRemoved(event) { const method = 'onResourceRemoved'; LOG.entry(method, event); - LOG.exit(); + LOG.exit(method); } } diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js index 8adb883ad2..60344d2f5a 100644 --- a/packages/composer-runtime/test/context.js +++ b/packages/composer-runtime/test/context.js @@ -75,6 +75,24 @@ describe('Context', () => { }); + describe('#getFunction', () => { + + it('should return the current function', () => { + context.function = 'suchfunc'; + context.getFunction().should.equal('suchfunc'); + }); + + }); + + describe('#getArguments', () => { + + it('should return the current arguments', () => { + context.arguments = ['arg1', 'arg2']; + context.getArguments().should.deep.equal(['arg1', 'arg2']); + }); + + }); + describe('#loadBusinessNetworkRecord', () => { it('should load the business record', () => { @@ -259,41 +277,104 @@ describe('Context', () => { describe('#loadCurrentParticipant', () => { - it('should return null if no identity is specified', () => { - let mockIdentityService = sinon.createStubInstance(IdentityService); - sandbox.stub(context, 'getIdentityService').returns(mockIdentityService); - let mockIdentityManager = sinon.createStubInstance(IdentityManager); - sandbox.stub(context, 'getIdentityManager').returns(mockIdentityManager); - mockIdentityService.getCurrentUserID.returns(null); - let mockParticipant = sinon.createStubInstance(Resource); - mockParticipant.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1'); - mockIdentityManager.getParticipant.withArgs('dogeid1').resolves(mockParticipant); + let mockIdentityManager; + let mockIdentityService; + let mockIdentity; + let mockParticipant; + + beforeEach(() => { + mockIdentityManager = sinon.createStubInstance(IdentityManager); + sinon.stub(context, 'getIdentityManager').returns(mockIdentityManager); + mockIdentityService = sinon.createStubInstance(IdentityService); + sinon.stub(context, 'getIdentityService').returns(mockIdentityService); + mockIdentity = sinon.createStubInstance(Resource); + mockParticipant = sinon.createStubInstance(Resource); + }); + + it('should get the identity, validate it, and get the participant', () => { + mockIdentityManager.getIdentity.resolves(mockIdentity); + mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant); return context.loadCurrentParticipant() - .should.eventually.be.equal(null); + .should.eventually.be.equal(mockParticipant) + .then(() => { + sinon.assert.calledOnce(mockIdentityManager.validateIdentity); + sinon.assert.calledWith(mockIdentityManager.validateIdentity, mockIdentity); + }); }); - it('should load the current participant if an identity is specified', () => { - let mockIdentityService = sinon.createStubInstance(IdentityService); - sandbox.stub(context, 'getIdentityService').returns(mockIdentityService); - let mockIdentityManager = sinon.createStubInstance(IdentityManager); - sandbox.stub(context, 'getIdentityManager').returns(mockIdentityManager); - mockIdentityService.getCurrentUserID.returns('dogeid1'); - let mockParticipant = sinon.createStubInstance(Resource); - mockParticipant.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1'); - mockIdentityManager.getParticipant.withArgs('dogeid1').resolves(mockParticipant); + it('should ignore an activation required message when calling activate identity', () => { + mockIdentityManager.getIdentity.resolves(mockIdentity); + mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant); + context.function = 'activateIdentity'; + const error = new Error('such error'); + error.activationRequired = true; + mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(error); return context.loadCurrentParticipant() - .should.eventually.be.equal(mockParticipant); + .should.eventually.be.equal(mockParticipant) + .then(() => { + sinon.assert.calledOnce(mockIdentityManager.validateIdentity); + sinon.assert.calledWith(mockIdentityManager.validateIdentity, mockIdentity); + }); }); - it('should throw an error if an invalid identity is specified', () => { - let mockIdentityService = sinon.createStubInstance(IdentityService); - sandbox.stub(context, 'getIdentityService').returns(mockIdentityService); - let mockIdentityManager = sinon.createStubInstance(IdentityManager); - sandbox.stub(context, 'getIdentityManager').returns(mockIdentityManager); - mockIdentityService.getCurrentUserID.returns('dogeid1'); - mockIdentityManager.getParticipant.withArgs('dogeid1').rejects(new Error('no such participant')); + it('should throw an activation required message when calling another function', () => { + mockIdentityManager.getIdentity.resolves(mockIdentity); + mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant); + context.function = 'bindIdentity'; + const error = new Error('such error'); + error.activationRequired = true; + mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(error); + return context.loadCurrentParticipant() + .should.be.rejectedWith(/such error/); + }); + + it('should throw an non-activation required message', () => { + mockIdentityManager.getIdentity.resolves(mockIdentity); + mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant); + mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(new Error('such error')); return context.loadCurrentParticipant() - .should.be.rejectedWith(/The identity may be invalid or may have been revoked/); + .should.be.rejectedWith(/such error/); + }); + + it('should ignore any errors from looking up the identity for admin users', () => { + let promise = Promise.resolve(); + ['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => { + mockIdentityService.getName.returns(admin); + mockIdentityManager.getIdentity.rejects(new Error('such error')); + promise = promise.then(() => { + return context.loadCurrentParticipant() + .should.eventually.be.null; + }); + }); + return promise; + }); + + it('should ignore any errors from validating the identity for admin users', () => { + let promise = Promise.resolve(); + ['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => { + mockIdentityService.getName.returns(admin); + mockIdentityManager.getIdentity.resolves(mockIdentity); + mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(new Error('such error')); + promise = promise.then(() => { + return context.loadCurrentParticipant() + .should.eventually.be.null; + }); + }); + return promise; + }); + + it('should ignore any errors from looking up the participant for admin users', () => { + let promise = Promise.resolve(); + ['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => { + mockIdentityService.getName.returns(admin); + mockIdentityManager.getIdentity.resolves(mockIdentity); + mockIdentityManager.getParticipant.withArgs(mockIdentity).rejects(new Error('such error')); + promise = promise.then(() => { + return context.loadCurrentParticipant() + .should.eventually.be.null; + }); + }); + return promise; }); }); @@ -427,7 +508,7 @@ describe('Context', () => { describe('#initialize', () => { - let mockBusinessNetworkDefinition, mockCompiledScriptBundle, mockCompiledQueryBundle, mockSystemRegistries, mockSystemIdentities, mockCompiledAclBundle; + let mockBusinessNetworkDefinition, mockCompiledScriptBundle, mockCompiledQueryBundle, mockSystemRegistries, mockCompiledAclBundle; beforeEach(() => { mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); @@ -443,8 +524,6 @@ describe('Context', () => { sinon.stub(context, 'getDataService').returns(mockDataService); mockSystemRegistries = sinon.createStubInstance(DataCollection); mockDataService.getCollection.withArgs('$sysregistries').resolves(mockSystemRegistries); - mockSystemIdentities = sinon.createStubInstance(DataCollection); - mockDataService.getCollection.withArgs('$sysidentities').resolves(mockSystemIdentities); }); it('should initialize the context', () => { @@ -466,7 +545,6 @@ describe('Context', () => { sinon.assert.calledOnce(context.loadCurrentParticipant); should.equal(context.participant, null); context.sysregistries.should.equal(mockSystemRegistries); - context.sysidentities.should.equal(mockSystemIdentities); }); }); @@ -498,11 +576,33 @@ describe('Context', () => { }); }); - it('should initialize the context with a specified system identities collection', () => { - let mockSystemIdentities2 = sinon.createStubInstance(DataCollection); - return context.initialize({ sysidentities: mockSystemIdentities2 }) + it('should initialize the context with the specified function name', () => { + return context.initialize({ function: 'suchfunc' }) + .then(() => { + context.function.should.equal('suchfunc'); + }); + }); + + it('should initialize the context with the original function name if no function name specified', () => { + context.function = 'suchfunc'; + return context.initialize({}) + .then(() => { + context.function.should.equal('suchfunc'); + }); + }); + + it('should initialize the context with the specified arguments', () => { + return context.initialize({ arguments: ['sucharg1', 'sucharg2'] }) + .then(() => { + context.arguments.should.deep.equal(['sucharg1', 'sucharg2']); + }); + }); + + it('should initialize the context with the original function name if no function name specified', () => { + context.arguments = ['sucharg1', 'sucharg2']; + return context.initialize({}) .then(() => { - context.sysidentities.should.equal(mockSystemIdentities2); + context.arguments.should.deep.equal(['sucharg1', 'sucharg2']); }); }); @@ -761,12 +861,12 @@ describe('Context', () => { describe('#getIdentityManager', () => { it('should return a new identity manager', () => { - let mockDataService = sinon.createStubInstance(DataService); - sinon.stub(context, 'getDataService').returns(mockDataService); + let mockIdentityService = sinon.createStubInstance(IdentityService); + sinon.stub(context, 'getIdentityService').returns(mockIdentityService); let mockRegistryManager = sinon.createStubInstance(RegistryManager); sinon.stub(context, 'getRegistryManager').returns(mockRegistryManager); - let mockSystemIdentities = sinon.createStubInstance(DataCollection); - sinon.stub(context, 'getSystemIdentities').returns(mockSystemIdentities); + let mockFactory = sinon.createStubInstance(Factory); + sinon.stub(context, 'getFactory').returns(mockFactory); context.getIdentityManager().should.be.an.instanceOf(IdentityManager); }); @@ -887,22 +987,6 @@ describe('Context', () => { }); - describe('#getSystemIdentities', () => { - - it('should throw if not initialized', () => { - (() => { - context.getSystemIdentities(); - }).should.throw(/must call initialize before calling this function/); - }); - - it('should return the system identities data collection', () => { - let mockSystemIdentities = sinon.createStubInstance(DataCollection); - context.sysidentities = mockSystemIdentities; - context.getSystemIdentities().should.equal(mockSystemIdentities); - }); - - }); - describe('#getEventNumber', () => { it('should get the current event number', () => { context.getEventNumber().should.equal(0); diff --git a/packages/composer-runtime/test/engine.identities.js b/packages/composer-runtime/test/engine.identities.js index 7fb424a291..e4bbdea38a 100644 --- a/packages/composer-runtime/test/engine.identities.js +++ b/packages/composer-runtime/test/engine.identities.js @@ -50,37 +50,73 @@ describe('EngineIdentities', () => { engine = new Engine(mockContainer); }); - describe('#addParticipantIdentity', () => { + describe('#issueIdentity', () => { it('should throw for invalid arguments', () => { - let result = engine.invoke(mockContext, 'addParticipantIdentity', ['no', 'args', 'supported', 'here']); - return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "addParticipantIdentity", expecting "\["participantId","userId"]"/); + let result = engine.invoke(mockContext, 'issueIdentity', ['no', 'args', 'supported', 'here']); + return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "issueIdentity", expecting "\["participantFQI","identityName"]"/); }); - it('should add the identity mapping', () => { - mockIdentityManager.addIdentityMapping.withArgs('org.doge.Doge#DOGE_1', 'dogeid1').resolves(); - return engine.invoke(mockContext, 'addParticipantIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']) + it('should issue the identity', () => { + mockIdentityManager.issueIdentity.withArgs('org.doge.Doge#DOGE_1', 'dogeid1').resolves(); + return engine.invoke(mockContext, 'issueIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']) .then(() => { - sinon.assert.calledOnce(mockIdentityManager.addIdentityMapping); - sinon.assert.calledWith(mockIdentityManager.addIdentityMapping, 'org.doge.Doge#DOGE_1', 'dogeid1'); + sinon.assert.calledOnce(mockIdentityManager.issueIdentity); + sinon.assert.calledWith(mockIdentityManager.issueIdentity, 'org.doge.Doge#DOGE_1', 'dogeid1'); }); }); }); - describe('#removeIdentity', () => { + describe('#bindIdentity', () => { it('should throw for invalid arguments', () => { - let result = engine.invoke(mockContext, 'removeIdentity', ['no', 'args', 'supported', 'here']); - return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "removeIdentity", expecting "\["userId"]"/); + let result = engine.invoke(mockContext, 'bindIdentity', ['no', 'args', 'supported', 'here']); + return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "bindIdentity", expecting "\["participantFQI","certificate"]"/); }); - it('should remove the identity mapping', () => { - mockIdentityManager.removeIdentityMapping.withArgs('dogeid1').resolves(); - return engine.invoke(mockContext, 'removeIdentity', ['dogeid1']) + it('should bind the identity', () => { + mockIdentityManager.bindIdentity.withArgs('org.doge.Doge#DOGE_1', '===== BEGIN CERTIFICATE =====').resolves(); + return engine.invoke(mockContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', '===== BEGIN CERTIFICATE =====']) .then(() => { - sinon.assert.calledOnce(mockIdentityManager.removeIdentityMapping); - sinon.assert.calledWith(mockIdentityManager.removeIdentityMapping, 'dogeid1'); + sinon.assert.calledOnce(mockIdentityManager.bindIdentity); + sinon.assert.calledWith(mockIdentityManager.bindIdentity); + }); + }); + + }); + + describe('#activateIdentity', () => { + + it('should throw for invalid arguments', () => { + let result = engine.invoke(mockContext, 'activateIdentity', ['no', 'args', 'supported', 'here']); + return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "activateIdentity", expecting "\[]"/); + }); + + it('should activate the identity', () => { + mockIdentityManager.activateIdentity.resolves(); + return engine.invoke(mockContext, 'activateIdentity', []) + .then(() => { + sinon.assert.calledOnce(mockIdentityManager.activateIdentity); + sinon.assert.calledWith(mockIdentityManager.activateIdentity); + }); + }); + + }); + + describe('#revokeIdentity', () => { + + it('should throw for invalid arguments', () => { + let result = engine.invoke(mockContext, 'revokeIdentity', ['no', 'args', 'supported', 'here']); + return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "revokeIdentity", expecting "\["identityId"]"/); + }); + + it('should revoke the identity', () => { + mockIdentityManager.revokeIdentity.withArgs('dogeid1').resolves(); + return engine.invoke(mockContext, 'revokeIdentity', ['dogeid1']) + .then(() => { + sinon.assert.calledOnce(mockIdentityManager.revokeIdentity); + sinon.assert.calledWith(mockIdentityManager.revokeIdentity, 'dogeid1'); }); }); diff --git a/packages/composer-runtime/test/engine.js b/packages/composer-runtime/test/engine.js index 0f255a295d..5fc5481141 100644 --- a/packages/composer-runtime/test/engine.js +++ b/packages/composer-runtime/test/engine.js @@ -135,7 +135,6 @@ describe('Engine', () => { it('should enable logging if logging specified on the init', () => { let sysdata = sinon.createStubInstance(DataCollection); let sysregistries = sinon.createStubInstance(DataCollection); - let sysidentities = sinon.createStubInstance(DataCollection); mockDataService.ensureCollection.withArgs('$sysdata').resolves(sysdata); let mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); let mockScriptManager = sinon.createStubInstance(ScriptManager); @@ -155,7 +154,6 @@ describe('Engine', () => { mockContext.getAclCompiler.returns(mockAclCompiler); sysdata.add.withArgs('businessnetwork', sinon.match.any).resolves(); mockDataService.ensureCollection.withArgs('$sysregistries').resolves(sysregistries); - mockDataService.ensureCollection.withArgs('$sysidentities').resolves(sysidentities); mockRegistryManager.ensure.withArgs('Transaction', 'default', 'Default Transaction Registry').resolves(); sandbox.stub(Context, 'cacheBusinessNetwork'); sandbox.stub(Context, 'cacheCompiledScriptBundle'); @@ -168,7 +166,7 @@ describe('Engine', () => { sinon.assert.calledOnce(mockLoggingService.setLogLevel); sinon.assert.calledWith(mockLoggingService.setLogLevel, 'DEBUG'); - sinon.assert.calledThrice(mockDataService.ensureCollection); + sinon.assert.calledTwice(mockDataService.ensureCollection); sinon.assert.calledWith(mockDataService.ensureCollection, '$sysdata'); sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive); sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, sinon.match((archive) => { @@ -187,18 +185,18 @@ describe('Engine', () => { sinon.assert.calledOnce(Context.cacheCompiledAclBundle); sinon.assert.calledWith(Context.cacheCompiledAclBundle, 'dc9c1c09907c36f5379d615ae61c02b46ba254d92edb77cb63bdcc5247ccd01c', mockCompiledAclBundle); sinon.assert.calledWith(mockDataService.ensureCollection, '$sysregistries'); - sinon.assert.calledWith(mockDataService.ensureCollection, '$sysidentities'); sinon.assert.calledOnce(mockRegistryManager.ensure); sinon.assert.calledWith(mockRegistryManager.ensure, 'Transaction', 'default', 'Default Transaction Registry'); sinon.assert.calledOnce(mockRegistryManager.createDefaults); sinon.assert.calledOnce(mockContext.initialize); sinon.assert.calledWith(mockContext.initialize, { + function: 'init', + arguments: ['aGVsbG8gd29ybGQ=','{"logLevel": "DEBUG"}'], businessNetworkDefinition: mockBusinessNetworkDefinition, compiledScriptBundle: mockCompiledScriptBundle, compiledQueryBundle: mockCompiledQueryBundle, compiledAclBundle: mockCompiledAclBundle, - sysregistries: sysregistries, - sysidentities: sysidentities + sysregistries: sysregistries }); sinon.assert.calledOnce(mockContext.transactionStart); sinon.assert.calledWith(mockContext.transactionStart, false); @@ -213,7 +211,6 @@ describe('Engine', () => { it('should create system collections and default registries', () => { let sysdata = sinon.createStubInstance(DataCollection); let sysregistries = sinon.createStubInstance(DataCollection); - let sysidentities = sinon.createStubInstance(DataCollection); mockDataService.ensureCollection.withArgs('$sysdata').resolves(sysdata); let mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); let mockScriptManager = sinon.createStubInstance(ScriptManager); @@ -233,7 +230,6 @@ describe('Engine', () => { mockContext.getAclCompiler.returns(mockAclCompiler); sysdata.add.withArgs('businessnetwork', sinon.match.any).resolves(); mockDataService.ensureCollection.withArgs('$sysregistries').resolves(sysregistries); - mockDataService.ensureCollection.withArgs('$sysidentities').resolves(sysidentities); mockRegistryManager.ensure.withArgs('Transaction', 'default', 'Default Transaction Registry').resolves(); sandbox.stub(Context, 'cacheBusinessNetwork'); sandbox.stub(Context, 'cacheCompiledScriptBundle'); @@ -241,7 +237,7 @@ describe('Engine', () => { return engine.init(mockContext, 'init', ['aGVsbG8gd29ybGQ=','{}']) .then(() => { sinon.assert.notCalled(mockLoggingService.setLogLevel); - sinon.assert.calledThrice(mockDataService.ensureCollection); + sinon.assert.calledTwice(mockDataService.ensureCollection); sinon.assert.calledWith(mockDataService.ensureCollection, '$sysdata'); sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive); sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, sinon.match((archive) => { @@ -256,18 +252,18 @@ describe('Engine', () => { sinon.assert.calledOnce(Context.cacheCompiledScriptBundle); sinon.assert.calledWith(Context.cacheCompiledScriptBundle, 'dc9c1c09907c36f5379d615ae61c02b46ba254d92edb77cb63bdcc5247ccd01c', mockCompiledScriptBundle); sinon.assert.calledWith(mockDataService.ensureCollection, '$sysregistries'); - sinon.assert.calledWith(mockDataService.ensureCollection, '$sysidentities'); sinon.assert.calledOnce(mockRegistryManager.ensure); sinon.assert.calledWith(mockRegistryManager.ensure, 'Transaction', 'default', 'Default Transaction Registry'); sinon.assert.calledOnce(mockRegistryManager.createDefaults); sinon.assert.calledOnce(mockContext.initialize); sinon.assert.calledWith(mockContext.initialize, { + function: 'init', + arguments: ['aGVsbG8gd29ybGQ=','{}'], businessNetworkDefinition: mockBusinessNetworkDefinition, compiledScriptBundle: mockCompiledScriptBundle, compiledQueryBundle: mockCompiledQueryBundle, compiledAclBundle: mockCompiledAclBundle, - sysregistries: sysregistries, - sysidentities: sysidentities + sysregistries: sysregistries }); sinon.assert.calledOnce(mockContext.transactionStart); sinon.assert.calledWith(mockContext.transactionStart, false); @@ -349,6 +345,10 @@ describe('Engine', () => { return engine.invoke(mockContext, 'test', []) .then(() => { sinon.assert.calledOnce(mockContext.initialize); + sinon.assert.calledWith(mockContext.initialize, { + function: 'test', + arguments: [] + }); sinon.assert.calledOnce(engine.test); sinon.assert.calledWith(engine.test, mockContext, []); sinon.assert.calledOnce(mockContext.transactionStart); @@ -421,6 +421,10 @@ describe('Engine', () => { engine.test = sinon.stub().resolves({}); return engine.query(mockContext, 'test', []) .then(() => { + sinon.assert.calledWith(mockContext.initialize, { + function: 'test', + arguments: [] + }); sinon.assert.calledOnce(mockContext.initialize); sinon.assert.calledOnce(engine.test); sinon.assert.calledWith(engine.test, mockContext, []); diff --git a/packages/composer-runtime/test/identitymanager.js b/packages/composer-runtime/test/identitymanager.js index 52201d64ed..1f0f7ce18a 100644 --- a/packages/composer-runtime/test/identitymanager.js +++ b/packages/composer-runtime/test/identitymanager.js @@ -14,12 +14,13 @@ 'use strict'; -const DataCollection = require('../lib/datacollection'); -const DataService = require('../lib/dataservice'); +const Context = require('../lib/context'); +const Factory = require('composer-common').Factory; const IdentityManager = require('../lib/identitymanager'); +const IdentityService = require('../lib/identityservice'); +const ModelManager = require('composer-common').ModelManager; const Registry = require('../lib/registry'); const RegistryManager = require('../lib/registrymanager'); -const Resource = require('composer-common').Resource; const chai = require('chai'); chai.should(); @@ -31,153 +32,378 @@ require('sinon-as-promised'); describe('IdentityManager', () => { - let mockDataService; - let mockSystemIdentities; + let mockContext; + let mockIdentityService; let mockRegistryManager; - let mockRegistry; + let mockIdentityRegistry; + let modelManager; + let factory; let identityManager; - let mockParticipant; beforeEach(() => { - mockDataService = sinon.createStubInstance(DataService); - mockSystemIdentities = sinon.createStubInstance(DataCollection); - mockDataService.getCollection.withArgs('$sysidentities').resolves(mockSystemIdentities); + mockContext = sinon.createStubInstance(Context); + mockIdentityService = sinon.createStubInstance(IdentityService); + mockContext.getIdentityService.returns(mockIdentityService); mockRegistryManager = sinon.createStubInstance(RegistryManager); - mockRegistry = sinon.createStubInstance(Registry); - mockRegistryManager.get.withArgs('Participant', 'org.doge.Doge').resolves(mockRegistry); - identityManager = new IdentityManager(mockDataService, mockRegistryManager, mockSystemIdentities); - mockParticipant = sinon.createStubInstance(Resource); - mockParticipant.getIdentifier.returns('DOGE_1'); - mockParticipant.getType.returns('Doge'); - mockParticipant.getNamespace.returns('org.doge'); - mockParticipant.getFullyQualifiedType.returns('org.doge.Doge'); - mockParticipant.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1'); + mockContext.getRegistryManager.returns(mockRegistryManager); + mockIdentityRegistry = sinon.createStubInstance(Registry); + mockRegistryManager.get.withArgs('Asset', 'org.hyperledger.composer.system.Identity').resolves(mockIdentityRegistry); + modelManager = new ModelManager(); + modelManager.addModelFile(` + namespace org.acme + participant SampleParticipant identified by participantId { + o String participantId + } + `); + factory = new Factory(modelManager); + mockContext.getFactory.returns(factory); + identityManager = new IdentityManager(mockContext); }); - describe('#addIdentityMapping', () => { + describe('#getIdentityRegistry', () => { - it('should add a new mapping for a user ID to a participant specified by a resource', () => { - // The participant exists. - mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant); - // An existing mapping for this user ID does not exist. - mockSystemIdentities.exists.withArgs('dogeid1').resolves(false); - return identityManager.addIdentityMapping(mockParticipant, 'dogeid1') + it('should get the identity registry', () => { + return identityManager.getIdentityRegistry() + .should.eventually.be.equal(mockIdentityRegistry); + }); + + }); + + describe('#getIdentity', () => { + + it('should look up an identity using the identifier', () => { + mockIdentityService.getIdentifier.returns('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63'); + let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', '7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63'); + mockIdentityRegistry.exists.withArgs('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63').resolves(true); + mockIdentityRegistry.get.withArgs('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63').resolves(identity); + return identityManager.getIdentity() + .should.eventually.be.equal(identity); + }); + + it('should look up an issued identity using the name and issuer', () => { + mockIdentityService.getIdentifier.returns('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63'); + mockIdentityRegistry.exists.withArgs('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63').resolves(false); + mockIdentityService.getName.returns('admin'); + mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'); + let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', '9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50'); + mockIdentityRegistry.exists.withArgs('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50').resolves(true); + mockIdentityRegistry.get.withArgs('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50').resolves(identity); + return identityManager.getIdentity() + .should.eventually.be.equal(identity); + }); + + it('should throw for an identity that does not exist', () => { + mockIdentityService.getIdentifier.returns('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63'); + mockIdentityRegistry.exists.withArgs('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63').resolves(false); + mockIdentityService.getName.returns('admin'); + mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'); + mockIdentityRegistry.exists.withArgs('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50').resolves(false); + return identityManager.getIdentity() + .should.be.rejectedWith(/The current identity has not been registered/); + }); + + }); + + describe('#validateIdentity', () => { + + let identity; + + beforeEach(() => { + identity = factory.newResource('org.hyperledger.composer.system', 'Identity', '9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50'); + }); + + it('should throw for a revoked identity', () => { + identity.state = 'REVOKED'; + (() => { + identityManager.validateIdentity(identity); + }).should.throw(/The current identity has been revoked/); + }); + + it('should throw for an issued identity that requires activation', () => { + identity.state = 'ISSUED'; + (() => { + identityManager.validateIdentity(identity); + }).should.throw(/The current identity must be activated \(ACTIVATION_REQUIRED\)/); + }); + + it('should throw for a bound identity that requires activation', () => { + identity.state = 'BOUND'; + (() => { + identityManager.validateIdentity(identity); + }).should.throw(/The current identity must be activated \(ACTIVATION_REQUIRED\)/); + }); + + it('should throw for an identity in an unknown state', () => { + identity.state = 'WOOPWOOP'; + (() => { + identityManager.validateIdentity(identity); + }).should.throw(/The current identity is in an unknown state/); + }); + + it('should not throw for an activated identity', () => { + identity.state = 'ACTIVATED'; + identityManager.validateIdentity(identity); + }); + + }); + + describe('#getParticipant', () => { + + let identity; + let participant; + let mockParticipantRegistry; + + beforeEach(() => { + identity = factory.newResource('org.hyperledger.composer.system', 'Identity', '9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50'); + participant = factory.newResource('org.acme', 'SampleParticipant', 'alice@email.com'); + identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com'); + mockParticipantRegistry = sinon.createStubInstance(Registry); + mockRegistryManager.get.withArgs('Participant', 'org.acme.SampleParticipant').resolves(mockParticipantRegistry); + }); + + it('should get the participant for the identity', () => { + mockParticipantRegistry.get.withArgs('alice@email.com').resolves(participant); + return identityManager.getParticipant(identity) + .should.eventually.be.equal(participant); + }); + + it('should throw if the participant does not exist', () => { + mockParticipantRegistry.get.withArgs('alice@email.com').rejects(new Error('such error')); + return identityManager.getParticipant(identity) + .should.be.rejectedWith(/The current identity is bound to a participant that does not exist/); + }); + + }); + + describe('#parseParticipant', () => { + + it('should throw for a missing hash', () => { + (() => { + identityManager.parseParticipant('org.acme.SampleParticipant_alice@email.com'); + }).should.throw(/Invalid fully qualified participant identifier/); + }); + + it('should parse the participant FQI into a relationship', () => { + const relationship = identityManager.parseParticipant('org.acme.SampleParticipant#alice@email.com'); + relationship.getNamespace().should.equal('org.acme'); + relationship.getType().should.equal('SampleParticipant'); + relationship.getIdentifier().should.equal('alice@email.com'); + }); + + }); + + describe('#issueIdentity', () => { + + it('should add a new identity to the identity registry', () => { + mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'); + return identityManager.issueIdentity('org.acme.SampleParticipant#alice@email.com', 'alice1') .then(() => { - sinon.assert.calledOnce(mockSystemIdentities.add); - sinon.assert.calledWith(mockSystemIdentities.add, 'dogeid1', { - participant: 'org.doge.Doge#DOGE_1' - }); + sinon.assert.calledOnce(mockIdentityRegistry.add); + const identity = mockIdentityRegistry.add.args[0][0]; + identity.getIdentifier().should.equal('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f'); + identity.name.should.equal('alice1'); + identity.issuer.should.equal('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'); + identity.certificate.should.equal(''); + identity.state.should.equal('ISSUED'); + identity.participant.getNamespace().should.equal('org.acme'); + identity.participant.getType().should.equal('SampleParticipant'); + identity.participant.getIdentifier().should.equal('alice@email.com'); }); }); - it('should add a new mapping for a user ID to a participant specified by an identifier', () => { - // The participant exists. - mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant); - // An existing mapping for this user ID does not exist. - mockSystemIdentities.exists.withArgs('dogeid1').resolves(false); - return identityManager.addIdentityMapping('org.doge.Doge#DOGE_1', 'dogeid1') + }); + + describe('#bindIdentity', () => { + + const pem = '-----BEGIN CERTIFICATE-----\nMIICGjCCAcCgAwIBAgIRANuOnVN+yd/BGyoX7ioEklQwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNjI2MTI0OTI2WhcNMjcwNjI0MTI0OTI2\nWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWQWRtaW5Ab3JnMS5leGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGu8KxBQ1GkxSTMVoLv7NXiYKWj5t6Dh\nWRTJBHnLkWV7lRUfYaKAKFadSii5M7Z7ZpwD8NS7IsMdPR6Z4EyGgwKjTTBLMA4G\nA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIBmrZau7BIB9\nrRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0gAMEUCIQC4sKQ6\nCEgqbTYe48az95W9/hnZ+7DI5eSnWUwV9vCd/gIgS5K6omNJydoFoEpaEIwM97uS\nXVMHPa0iyC497vdNURA=\n-----END CERTIFICATE-----\n'; + + it('should add a new identity to the identity registry', () => { + mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'); + return identityManager.bindIdentity('org.acme.SampleParticipant#alice@email.com', pem) .then(() => { - sinon.assert.calledOnce(mockSystemIdentities.add); - sinon.assert.calledWith(mockSystemIdentities.add, 'dogeid1', { - participant: 'org.doge.Doge#DOGE_1' - }); + sinon.assert.calledOnce(mockIdentityRegistry.add); + const identity = mockIdentityRegistry.add.args[0][0]; + identity.getIdentifier().should.equal('2be26f6d4757b49fbae46f9fbb0c225b2f77508a882e4dd899700b56f62ad639'); + identity.name.should.equal(''); + identity.issuer.should.equal(''); + identity.certificate.should.equal(pem); + identity.state.should.equal('BOUND'); + identity.participant.getNamespace().should.equal('org.acme'); + identity.participant.getType().should.equal('SampleParticipant'); + identity.participant.getIdentifier().should.equal('alice@email.com'); }); }); - it('should throw if the specified participant does not exist', () => { - // The participant does not exist. - mockRegistry.get.withArgs('DOGE_1').rejects(new Error('does not exist')); - // An existing mapping for this user ID does not exist. - mockSystemIdentities.exists.withArgs('dogeid1').resolves(false); - return identityManager.addIdentityMapping('org.doge.Doge#DOGE_1', 'dogeid1') - .should.be.rejectedWith(/does not exist/); + }); + + describe('#activateIdentity', () => { + + const pem = '-----BEGIN CERTIFICATE-----\nMIICGjCCAcCgAwIBAgIRANuOnVN+yd/BGyoX7ioEklQwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNjI2MTI0OTI2WhcNMjcwNjI0MTI0OTI2\nWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWQWRtaW5Ab3JnMS5leGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGu8KxBQ1GkxSTMVoLv7NXiYKWj5t6Dh\nWRTJBHnLkWV7lRUfYaKAKFadSii5M7Z7ZpwD8NS7IsMdPR6Z4EyGgwKjTTBLMA4G\nA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIBmrZau7BIB9\nrRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0gAMEUCIQC4sKQ6\nCEgqbTYe48az95W9/hnZ+7DI5eSnWUwV9vCd/gIgS5K6omNJydoFoEpaEIwM97uS\nXVMHPa0iyC497vdNURA=\n-----END CERTIFICATE-----\n'; + + it('should activate an issued identity', () => { + let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f'); + identity.name = 'alice1'; + identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'; + identity.certificate = ''; + identity.state = 'ISSUED'; + identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com'); + sinon.stub(identityManager, 'getIdentity').resolves(identity); + sinon.stub(identityManager, 'activateIssuedIdentity').resolves(); + return identityManager.activateIdentity() + .then(() => { + sinon.assert.calledOnce(identityManager.activateIssuedIdentity); + sinon.assert.calledWith(identityManager.activateIssuedIdentity, mockIdentityRegistry, identity); + }); }); - it('should throw if the specified participant ID is invalid', () => { - // The participant does not exist. - mockRegistry.get.withArgs('DOGE_1').rejects(new Error('does not exist')); - // An existing mapping for this user ID does not exist. - mockSystemIdentities.exists.withArgs('dogeid1').resolves(false); - (() => { - identityManager.addIdentityMapping('org.doge.Doge$DOGE_1', 'dogeid1'); - }).should.throw(/Invalid fully qualified participant identifier/); + it('should activate a bound identity', () => { + let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f'); + identity.name = 'alice1'; + identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'; + identity.certificate = pem; + identity.state = 'BOUND'; + identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com'); + sinon.stub(identityManager, 'getIdentity').resolves(identity); + sinon.stub(identityManager, 'activateBoundIdentity').resolves(); + return identityManager.activateIdentity() + .then(() => { + sinon.assert.calledOnce(identityManager.activateBoundIdentity); + sinon.assert.calledWith(identityManager.activateBoundIdentity, mockIdentityRegistry, identity); + }); }); - it('should throw if the specified user ID is already mapped', () => { - // The participant exists. - mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant); - // An existing mapping for this user ID does exist. - mockSystemIdentities.exists.withArgs('dogeid1').resolves(true); - return identityManager.addIdentityMapping('org.doge.Doge#DOGE_1', 'dogeid1') - .should.be.rejectedWith(/Found an existing mapping for user ID/); + it('should throw for an already activated identity', () => { + let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f'); + identity.name = 'alice1'; + identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'; + identity.certificate = pem; + identity.state = 'ACTIVATED'; + identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com'); + sinon.stub(identityManager, 'getIdentity').resolves(identity); + return identityManager.activateIdentity() + .should.be.rejectedWith(/The current identity cannot be activated because it is in an unknown state/); }); }); - describe('#removeIdentityMapping', () => { + describe('#activateIssuedIdentity', () => { + + const pem = '-----BEGIN CERTIFICATE-----\nMIICGjCCAcCgAwIBAgIRANuOnVN+yd/BGyoX7ioEklQwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNjI2MTI0OTI2WhcNMjcwNjI0MTI0OTI2\nWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWQWRtaW5Ab3JnMS5leGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGu8KxBQ1GkxSTMVoLv7NXiYKWj5t6Dh\nWRTJBHnLkWV7lRUfYaKAKFadSii5M7Z7ZpwD8NS7IsMdPR6Z4EyGgwKjTTBLMA4G\nA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIBmrZau7BIB9\nrRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0gAMEUCIQC4sKQ6\nCEgqbTYe48az95W9/hnZ+7DI5eSnWUwV9vCd/gIgS5K6omNJydoFoEpaEIwM97uS\nXVMHPa0iyC497vdNURA=\n-----END CERTIFICATE-----\n'; + + let identity; - it('should remove an existing mapping for a user ID to a participant', () => { - // An existing mapping for this user ID does exist. - mockSystemIdentities.exists.withArgs('dogeid1').resolves(true); - return identityManager.removeIdentityMapping('dogeid1') + beforeEach(() => { + identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f'); + identity.name = 'alice1'; + identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'; + identity.certificate = ''; + identity.state = 'ISSUED'; + identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com'); + }); + + it('should throw for an invalid issuer', () => { + mockIdentityService.getIdentifier.returns('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50'); + mockIdentityService.getName.returns('alice1'); + mockIdentityService.getIssuer.returns('f15148476b739e6329781a5b963cc30dc583a124408da8222418a7553843c251'); + mockIdentityService.getCertificate.returns(pem); + mockIdentityRegistry.remove.resolves(); + mockIdentityRegistry.add.resolves(); + (() => { + identityManager.activateIssuedIdentity(mockIdentityRegistry, identity); + }).should.throw(/The current identity cannot be activated because the issuer is invalid/); + }); + + it('should remove and add the updated identity in the identity registry', () => { + mockIdentityService.getIdentifier.returns('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50'); + mockIdentityService.getName.returns('alice1'); + mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'); + mockIdentityService.getCertificate.returns(pem); + mockIdentityRegistry.remove.resolves(); + mockIdentityRegistry.add.resolves(); + return identityManager.activateIssuedIdentity(mockIdentityRegistry, identity) .then(() => { - sinon.assert.calledOnce(mockSystemIdentities.remove); - sinon.assert.calledWith(mockSystemIdentities.remove, 'dogeid1'); + sinon.assert.calledOnce(mockIdentityRegistry.remove); + sinon.assert.calledWith(mockIdentityRegistry.remove, identity); + sinon.assert.calledOnce(mockIdentityRegistry.add); + const newIdentity = mockIdentityRegistry.add.args[0][0]; + newIdentity.getIdentifier().should.equal('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50'); + newIdentity.name.should.equal('alice1'); + newIdentity.issuer.should.equal('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'); + newIdentity.certificate.should.equal(pem); + newIdentity.state.should.equal('ACTIVATED'); + newIdentity.participant.getNamespace().should.equal('org.acme'); + newIdentity.participant.getType().should.equal('SampleParticipant'); + newIdentity.participant.getIdentifier().should.equal('alice@email.com'); }); }); - it('should not throw if an existing mapping for a user ID does not exist', () => { - // An existing mapping for this user ID does not exist. - mockSystemIdentities.exists.withArgs('dogeid1').resolves(false); - return identityManager.removeIdentityMapping('dogeid1'); + }); + + describe('#activateBoundIdentity', () => { + + const pem = '-----BEGIN CERTIFICATE-----\nMIICGjCCAcCgAwIBAgIRANuOnVN+yd/BGyoX7ioEklQwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNjI2MTI0OTI2WhcNMjcwNjI0MTI0OTI2\nWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWQWRtaW5Ab3JnMS5leGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGu8KxBQ1GkxSTMVoLv7NXiYKWj5t6Dh\nWRTJBHnLkWV7lRUfYaKAKFadSii5M7Z7ZpwD8NS7IsMdPR6Z4EyGgwKjTTBLMA4G\nA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIBmrZau7BIB9\nrRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0gAMEUCIQC4sKQ6\nCEgqbTYe48az95W9/hnZ+7DI5eSnWUwV9vCd/gIgS5K6omNJydoFoEpaEIwM97uS\nXVMHPa0iyC497vdNURA=\n-----END CERTIFICATE-----\n'; + + let identity; + + beforeEach(() => { + identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f'); + identity.name = 'alice1'; + identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'; + identity.certificate = pem; + identity.state = 'ISSUED'; + identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com'); + }); + + it('should update the identity in the identity registry', () => { + mockIdentityService.getName.returns('alice1'); + mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'); + mockIdentityRegistry.update.resolves(); + return identityManager.activateBoundIdentity(mockIdentityRegistry, identity) + .then(() => { + sinon.assert.calledOnce(mockIdentityRegistry.update); + const newIdentity = mockIdentityRegistry.update.args[0][0]; + newIdentity.getIdentifier().should.equal('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f'); + newIdentity.name.should.equal('alice1'); + newIdentity.issuer.should.equal('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417'); + newIdentity.certificate.should.equal(pem); + newIdentity.state.should.equal('ACTIVATED'); + newIdentity.participant.getNamespace().should.equal('org.acme'); + newIdentity.participant.getType().should.equal('SampleParticipant'); + newIdentity.participant.getIdentifier().should.equal('alice@email.com'); + }); }); }); - describe('#getParticipant', () => { + describe('#revokeIdentity', () => { - it('should resolve to a participant for an existing mapping and participant', () => { - // An existing mapping for this user ID does exist. - mockSystemIdentities.get.withArgs('dogeid1').resolves({ - participant: 'org.doge.Doge#DOGE_1' - }); - // The participant exists. - mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant); - return identityManager.getParticipant('dogeid1') - .then((participant) => { - participant.should.equal(mockParticipant); - }); + let identity; + + beforeEach(() => { + identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f'); + identity.name = ''; + identity.issuer = ''; + identity.certificate = ''; + identity.state = 'ISSUED'; + identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com'); + mockIdentityRegistry.get.withArgs('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f').resolves(identity); }); - it('should throw an error for a missing mapping', () => { - // An existing mapping for this user ID does not exist. - mockSystemIdentities.get.withArgs('dogeid1').rejects(new Error('no such mapping')); - // The participant exists. - mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant); - return identityManager.getParticipant('dogeid1') - .should.be.rejectedWith(/no such mapping/); - }); - - it('should throw an error for an invalid mapping', () => { - // An existing mapping for this user ID does not exist. - mockSystemIdentities.get.withArgs('dogeid1').resolves({ - participant: 'org.doge.Doge@DOGE_1' - }); - // The participant exists. - mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant); - return identityManager.getParticipant('dogeid1') - .should.be.rejectedWith(/Invalid fully qualified participant identifier/); - }); - - it('should throw an error for an existing mapping but missing participant', () => { - // An existing mapping for this user ID does exist. - mockSystemIdentities.get.withArgs('dogeid1').resolves({ - participant: 'org.doge.Doge#DOGE_1' - }); - // The participant does not exist. - mockRegistry.get.withArgs('DOGE_1').rejects(new Error('no such participant')); - return identityManager.getParticipant('dogeid1') - .should.be.rejectedWith(/no such participant/); + it('should update the identity in the identity registry', () => { + mockIdentityRegistry.update.resolves(); + return identityManager.revokeIdentity('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f') + .then(() => { + sinon.assert.calledOnce(mockIdentityRegistry.update); + const newIdentity = mockIdentityRegistry.update.args[0][0]; + newIdentity.getIdentifier().should.equal('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f'); + newIdentity.name.should.equal(''); + newIdentity.issuer.should.equal(''); + newIdentity.certificate.should.equal(''); + newIdentity.state.should.equal('REVOKED'); + newIdentity.participant.getNamespace().should.equal('org.acme'); + newIdentity.participant.getType().should.equal('SampleParticipant'); + newIdentity.participant.getIdentifier().should.equal('alice@email.com'); + }); }); }); diff --git a/packages/composer-runtime/test/identityservice.js b/packages/composer-runtime/test/identityservice.js index 63688dad79..9dcc94e6fe 100644 --- a/packages/composer-runtime/test/identityservice.js +++ b/packages/composer-runtime/test/identityservice.js @@ -22,11 +22,41 @@ describe('IdentityService', () => { let identityService = new IdentityService(); - describe('#getCurrentUserID', () => { + describe('#getIdentifier', () => { it('should throw as abstract method', () => { (() => { - identityService.getCurrentUserID(); + identityService.getIdentifier(); + }).should.throw(/abstract function called/); + }); + + }); + + describe('#getName', () => { + + it('should throw as abstract method', () => { + (() => { + identityService.getName(); + }).should.throw(/abstract function called/); + }); + + }); + + describe('#getIssuer', () => { + + it('should throw as abstract method', () => { + (() => { + identityService.getIssuer(); + }).should.throw(/abstract function called/); + }); + + }); + + describe('#getCertificate', () => { + + it('should throw as abstract method', () => { + (() => { + identityService.getCertificate(); }).should.throw(/abstract function called/); }); diff --git a/packages/composer-systests/systest/identities.js b/packages/composer-systests/systest/identities.js index 48ae3afe63..ba97e4f077 100644 --- a/packages/composer-systests/systest/identities.js +++ b/packages/composer-systests/systest/identities.js @@ -127,7 +127,7 @@ describe('Identity system tests', () => { client = result; return client.ping(); }) - .should.be.rejectedWith(/The identity may be invalid or may have been revoked/); + .should.be.rejectedWith(/The current identity is bound to a participant that does not exist/); }); it('should issue an identity and make the participant available for transaction processor functions', () => { @@ -180,7 +180,7 @@ describe('Identity system tests', () => { let transaction = factory.newTransaction('systest.identities', 'SampleTransaction'); return client.submitTransaction(transaction); }) - .should.be.rejectedWith(/The identity may be invalid or may have been revoked/); + .should.be.rejectedWith(/The current identity is bound to a participant that does not exist/); }); }); From ab3d3e3f0fc55e2ac91e41bac8f16442ea9cf8ab Mon Sep 17 00:00:00 2001 From: Simon Stone Date: Wed, 12 Jul 2017 22:25:09 +0100 Subject: [PATCH 2/7] Get identities working again on embedded and web runtimes (#1546) * Cannot activate issued identity as identity not found * Get identities working again on embedded and web runtimes --- .../lib/embeddedconnection.js | 109 +++++++++------ .../lib/embeddedsecuritycontext.js | 14 +- .../test/embeddedconnection.js | 113 ++++++++------- .../test/embeddedsecuritycontext.js | 20 ++- .../lib/webconnection.js | 129 +++++++++++------- .../lib/websecuritycontext.js | 14 +- .../test/webconnection.js | 119 +++++++++------- .../test/websecuritycontext.js | 20 ++- .../lib/embeddedcontext.js | 6 +- .../lib/embeddedidentityservice.js | 39 ++++-- .../test/embeddedcontext.js | 21 +-- .../test/embeddedidentityservice.js | 51 ++++++- .../composer-runtime-web/lib/webcontext.js | 6 +- .../lib/webidentityservice.js | 39 ++++-- .../composer-runtime-web/test/webcontext.js | 19 +-- .../test/webidentityservice.js | 42 +++++- packages/composer-runtime/lib/context.js | 11 +- packages/composer-runtime/test/context.js | 2 +- 18 files changed, 495 insertions(+), 279 deletions(-) diff --git a/packages/composer-connector-embedded/lib/embeddedconnection.js b/packages/composer-connector-embedded/lib/embeddedconnection.js index cc5f0f541c..ad56eb8d18 100644 --- a/packages/composer-connector-embedded/lib/embeddedconnection.js +++ b/packages/composer-connector-embedded/lib/embeddedconnection.js @@ -15,6 +15,7 @@ 'use strict'; const Connection = require('composer-common').Connection; +const createHash = require('sha.js'); const Engine = require('composer-runtime').Engine; const EmbeddedContainer = require('composer-runtime-embedded').EmbeddedContainer; const EmbeddedContext = require('composer-runtime-embedded').EmbeddedContext; @@ -28,6 +29,9 @@ const businessNetworks = {}; // A mapping of chaincode IDs to their instance objects. const chaincodes = {}; +// The issuer for all identities. +const DEFAULT_ISSUER = createHash('sha256').update('org1').digest('hex'); + /** * Base class representing a connection to a business network. * @protected @@ -148,20 +152,19 @@ class EmbeddedConnection extends Connection { * object representing the logged in participant, or rejected with a login error. */ login(enrollmentID, enrollmentSecret) { - // The 'admin' ID is special for the moment as it is not bound to a participant. - let result = new EmbeddedSecurityContext(this, enrollmentID !== 'admin' ? enrollmentID : null); if (!this.businessNetworkIdentifier) { return this.testIdentity(enrollmentID, enrollmentSecret) - .then(() => { - return result; + .then((identity) => { + return new EmbeddedSecurityContext(this, identity); }); } return this.testIdentity(enrollmentID, enrollmentSecret) - .then(() => { + .then((identity) => { let chaincodeUUID = EmbeddedConnection.getBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile); if (!chaincodeUUID) { throw new Error(`No chaincode ID found for business network '${this.businessNetworkIdentifier}'`); } + const result = new EmbeddedSecurityContext(this, identity); result.setChaincodeID(chaincodeUUID); return result; }); @@ -177,12 +180,12 @@ class EmbeddedConnection extends Connection { */ deploy(securityContext, businessNetwork, deployOptions) { let container = EmbeddedConnection.createContainer(); - let userID = securityContext.getUserID(); + let identity = securityContext.getIdentity(); let chaincodeUUID = container.getUUID(); let engine = EmbeddedConnection.createEngine(container); EmbeddedConnection.addBusinessNetwork(businessNetwork.getName(), this.connectionProfile, chaincodeUUID); EmbeddedConnection.addChaincode(chaincodeUUID, container, engine); - let context = new EmbeddedContext(engine, userID, this); + let context = new EmbeddedContext(engine, identity, this); return businessNetwork.toArchive({ date: new Date(545184000000) }) .then((businessNetworkArchive) => { const initArgs = {}; @@ -244,10 +247,10 @@ class EmbeddedConnection extends Connection { * chaincode function once it has been invoked, or rejected with an error. */ queryChainCode(securityContext, functionName, args) { - let userID = securityContext.getUserID(); + let identity = securityContext.getIdentity(); let chaincodeUUID = securityContext.getChaincodeID(); let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID); - let context = new EmbeddedContext(chaincode.engine, userID, this); + let context = new EmbeddedContext(chaincode.engine, identity, this); return chaincode.engine.query(context, functionName, args) .then((data) => { return Buffer.from(JSON.stringify(data)); @@ -263,10 +266,10 @@ class EmbeddedConnection extends Connection { * has been invoked, or rejected with an error. */ invokeChainCode(securityContext, functionName, args) { - let userID = securityContext.getUserID(); + let identity = securityContext.getIdentity(); let chaincodeUUID = securityContext.getChaincodeID(); let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID); - let context = new EmbeddedContext(chaincode.engine, userID, this); + let context = new EmbeddedContext(chaincode.engine, identity, this); return chaincode.engine.invoke(context, functionName, args) .then((data) => { return undefined; @@ -275,47 +278,57 @@ class EmbeddedConnection extends Connection { /** * Get the data collection that stores identities. - * @return {DataCollection} The data collection that stores identities. + * @return {Promise} A promise that is resolved with the data collection + * that stores identities. */ getIdentities() { - return this.dataService.existsCollection('identities') - .then((exists) => { - if (exists) { - return this.dataService.getCollection('identities'); - } else { - return this.dataService.createCollection('identities'); - } - }); + return this.dataService.ensureCollection('identities'); } /** - * Test the specified user ID and secret to ensure that it is valid. - * @param {string} userID The user ID. - * @param {string} userSecret The user secret. - * @return {Promise} A promise that is resolved if the user ID and secret - * is valid, or rejected with an error. + * Get the identity for the specified name. + * @param {string} identityName The name for the identity. + * @return {Promise} A promise that is resolved with the identity, or + * rejected with an error. */ - testIdentity(userID, userSecret) { - // The 'admin' ID is special for the moment as it is not bound to a participant. - if (userID === 'admin') { - return Promise.resolve(); + getIdentity(identityName) { + if (identityName === 'admin') { + return Promise.resolve({ + identifier: '', + name: 'admin', + issuer: DEFAULT_ISSUER, + secret: 'adminpw' + }); } return this.getIdentities() .then((identities) => { - return identities.get(userID); - }) + return identities.get(identityName); + }); + } + + /** + * Test the specified identity name and secret to ensure that it is valid. + * @param {string} identityName The name for the identity. + * @param {string} identitySecret The secret for the identity. + * @return {Promise} A promise that is resolved if the user ID and secret + * is valid, or rejected with an error. + */ + testIdentity(identityName, identitySecret) { + return this.getIdentity(identityName) .then((identity) => { - if (identity.userSecret !== userSecret) { - throw new Error(`The user secret ${userSecret} specified for the user ID ${userID} does not match the stored user secret ${identity.userSecret}`); + if (identityName !== 'admin') { + if (identity.secret !== identitySecret) { + throw new Error(`The secret ${identitySecret} specified for the identity ${identityName} does not match the stored secret ${identity.secret}`); + } } + return identity; }); - } /** - * Create a new identity for the specified user ID. + * Create a new identity for the specified name. * @param {SecurityContext} securityContext The participant's security context. - * @param {string} userID The user ID. + * @param {string} identityName The name for the new identity. * @param {object} [options] Options for the new identity. * @param {boolean} [options.issuer] Whether or not the new identity should have * permissions to create additional new identities. False by default. @@ -324,22 +337,32 @@ class EmbeddedConnection extends Connection { * @return {Promise} A promise that is resolved with a generated user * secret once the new identity has been created, or rejected with an error. */ - createIdentity(securityContext, userID, options) { + createIdentity(securityContext, identityName, options) { let identities; return this.getIdentities() .then((identities_) => { identities = identities_; - return identities.exists(userID); + return identities.exists(identityName); }) .then((exists) => { if (exists) { - return identities.get(userID); + return identities.get(identityName); } - const userSecret = uuid.v4().substring(0, 8); - const identity = { userID: userID, userSecret: userSecret }; - return identities.add(userID, identity) + const identifier = createHash('sha256').update(uuid.v4()).digest('hex'); + const secret = uuid.v4().substring(0, 8); + const identity = { + identifier, + name: identityName, + issuer: DEFAULT_ISSUER, + secret, + certificate: '' + }; + return identities.add(identityName, identity) .then(() => { - return identity; + return { + userID: identity.name, + userSecret: identity.secret + }; }); }); } diff --git a/packages/composer-connector-embedded/lib/embeddedsecuritycontext.js b/packages/composer-connector-embedded/lib/embeddedsecuritycontext.js index f89ed9e435..671a1d7c3b 100644 --- a/packages/composer-connector-embedded/lib/embeddedsecuritycontext.js +++ b/packages/composer-connector-embedded/lib/embeddedsecuritycontext.js @@ -24,20 +24,20 @@ class EmbeddedSecurityContext extends SecurityContext { /** * Constructor. * @param {Connection} connection The owning connection. - * @param {String} userID The current user ID. + * @param {Object} identity The current identity. */ - constructor(connection, userID) { + constructor(connection, identity) { super(connection); - this.userID = userID; + this.identity = identity; this.chaincodeID = null; } /** - * Get the current user ID. - * @return {string} The current user ID. + * Get the current identity. + * @return {string} The current identity. */ - getUserID() { - return this.userID; + getIdentity() { + return this.identity; } /** diff --git a/packages/composer-connector-embedded/test/embeddedconnection.js b/packages/composer-connector-embedded/test/embeddedconnection.js index fd69228848..bd3b395791 100644 --- a/packages/composer-connector-embedded/test/embeddedconnection.js +++ b/packages/composer-connector-embedded/test/embeddedconnection.js @@ -35,6 +35,14 @@ require('sinon-as-promised'); describe('EmbeddedConnection', () => { + const identity = { + identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a', + name: 'bob1', + issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665', + secret: 'suchsecret', + certificate: '' + }; + let sandbox; let mockConnectionManager; let mockSecurityContext; @@ -94,25 +102,13 @@ describe('EmbeddedConnection', () => { sandbox.stub(connection, 'testIdentity').resolves(); }); - it('should return a new security context with a null user ID if the admin ID is specified', () => { - connection = new EmbeddedConnection(mockConnectionManager, 'devFabric1'); - sandbox.stub(connection, 'testIdentity').resolves(); - return connection.login('admin', 'suchs3cret') - .then((securityContext) => { - securityContext.should.be.an.instanceOf(EmbeddedSecurityContext); - should.equal(securityContext.getUserID(), null); - should.equal(securityContext.getChaincodeID(), null); - sinon.assert.calledWith(connection.testIdentity, 'admin', 'suchs3cret'); - }); - }); - it('should return a new security context with a null chaincode ID if the business network was not specified', () => { connection = new EmbeddedConnection(mockConnectionManager, 'devFabric1'); - sandbox.stub(connection, 'testIdentity').resolves(); + sandbox.stub(connection, 'testIdentity').resolves(identity); return connection.login('doge', 'suchs3cret') .then((securityContext) => { securityContext.should.be.an.instanceOf(EmbeddedSecurityContext); - securityContext.getUserID().should.equal('doge'); + securityContext.getIdentity().should.deep.equal(identity); should.equal(securityContext.getChaincodeID(), null); sinon.assert.calledWith(connection.testIdentity, 'doge', 'suchs3cret'); }); @@ -143,7 +139,7 @@ describe('EmbeddedConnection', () => { mockBusinessNetwork.getName.returns('testnetwork'); let mockContainer = sinon.createStubInstance(EmbeddedContainer); mockContainer.getUUID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f'); - mockSecurityContext.getUserID.returns('bob1'); + mockSecurityContext.getIdentity.returns(identity); sandbox.stub(EmbeddedConnection, 'createContainer').returns(mockContainer); let mockEngine = sinon.createStubInstance(Engine); mockEngine.getContainer.returns(mockContainer); @@ -155,7 +151,7 @@ describe('EmbeddedConnection', () => { sinon.assert.calledOnce(mockEngine.init); sinon.assert.calledWith(mockEngine.init, sinon.match((context) => { context.should.be.an.instanceOf(Context); - context.getIdentityService().getCurrentUserID().should.equal('bob1'); + context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); return true; }), 'init', ['aGVsbG8gd29ybGQ=', '{}']); sinon.assert.calledOnce(connection.ping); @@ -245,7 +241,7 @@ describe('EmbeddedConnection', () => { mockEngine.getContainer.returns(mockContainer); EmbeddedConnection.addBusinessNetwork('org.acme.Business', 'devFabric1', '6eeb8858-eced-4a32-b1cd-2491f1e3718f'); EmbeddedConnection.addChaincode('6eeb8858-eced-4a32-b1cd-2491f1e3718f', mockContainer, mockEngine); - mockSecurityContext.getUserID.returns('bob1'); + mockSecurityContext.getIdentity.returns(identity); mockSecurityContext.getChaincodeID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f'); mockEngine.query.resolves({ test: 'data from engine' }); return connection.queryChainCode(mockSecurityContext, 'testFunction', ['arg1', 'arg2']) @@ -253,7 +249,7 @@ describe('EmbeddedConnection', () => { sinon.assert.calledOnce(mockEngine.query); sinon.assert.calledWith(mockEngine.query, sinon.match((context) => { context.should.be.an.instanceOf(Context); - context.getIdentityService().getCurrentUserID().should.equal('bob1'); + context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); return true; }), 'testFunction', ['arg1', 'arg2']); result.should.be.an.instanceOf(Buffer); @@ -271,7 +267,7 @@ describe('EmbeddedConnection', () => { mockEngine.getContainer.returns(mockContainer); EmbeddedConnection.addBusinessNetwork('org.acme.Business', 'devFabric1', '6eeb8858-eced-4a32-b1cd-2491f1e3718f'); EmbeddedConnection.addChaincode('6eeb8858-eced-4a32-b1cd-2491f1e3718f', mockContainer, mockEngine); - mockSecurityContext.getUserID.returns('bob1'); + mockSecurityContext.getIdentity.returns(identity); mockSecurityContext.getChaincodeID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f'); mockEngine.invoke.resolves({ test: 'data from engine' }); return connection.invokeChainCode(mockSecurityContext, 'testFunction', ['arg1', 'arg2']) @@ -279,7 +275,7 @@ describe('EmbeddedConnection', () => { sinon.assert.calledOnce(mockEngine.invoke); sinon.assert.calledWith(mockEngine.invoke, sinon.match((context) => { context.should.be.an.instanceOf(Context); - context.getIdentityService().getCurrentUserID().should.equal('bob1'); + context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); return true; }), 'testFunction', ['arg1', 'arg2']); should.equal(result, undefined); @@ -296,21 +292,10 @@ describe('EmbeddedConnection', () => { beforeEach(() => { connection.dataService = mockDataService = sinon.createStubInstance(DataService); mockIdentitiesDataCollection = sinon.createStubInstance(DataCollection); - }); - it('should create and return the identities collection if it does not exist', () => { - mockDataService.existsCollection.withArgs('identities').resolves(false); - mockDataService.createCollection.withArgs('identities').resolves(mockIdentitiesDataCollection); - return connection.getIdentities() - .then((identities) => { - identities.should.equal(mockIdentitiesDataCollection); - }); - }); - - it('should return the existing identities collection if it already exists', () => { - mockDataService.existsCollection.withArgs('identities').resolves(true); - mockDataService.getCollection.withArgs('identities').resolves(mockIdentitiesDataCollection); + it('should ensure and return the identities collection', () => { + mockDataService.ensureCollection.withArgs('identities').resolves(mockIdentitiesDataCollection); return connection.getIdentities() .then((identities) => { identities.should.equal(mockIdentitiesDataCollection); @@ -319,34 +304,57 @@ describe('EmbeddedConnection', () => { }); - describe('#testIdentity', () => { + describe('#getIdentity', () => { let mockIdentitiesDataCollection; beforeEach(() => { mockIdentitiesDataCollection = sinon.createStubInstance(DataCollection); - sandbox.stub(connection, 'getIdentities').resolves(mockIdentitiesDataCollection); + sinon.stub(connection, 'getIdentities').resolves(mockIdentitiesDataCollection); }); - it('should resolve if the user ID is admin', () => { - return connection.testIdentity('admin', 'password'); + it('should return the hardcoded admin identity', () => { + return connection.getIdentity('admin') + .should.eventually.be.deep.equal({ + identifier: '', + name: 'admin', + issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e', + secret: 'adminpw' + }); }); - it('should resolve if the user ID exists', () => { - mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'password' }); - return connection.testIdentity('doge', 'password'); + it('should return the specified identity', () => { + mockIdentitiesDataCollection.get.withArgs('bob1').resolves(identity); + return connection.getIdentity('bob1') + .should.eventually.be.equal(identity); + }); + + }); + + describe('#testIdentity', () => { + + it('should not check the secret if the name is admin', () => { + const identity = { + identifier: '', + name: 'admin', + issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e', + secret: 'adminpw' + }; + sinon.stub(connection, 'getIdentity').resolves(identity); + return connection.testIdentity('admin', 'blahblah') + .should.eventually.be.equal(identity); }); - it('should throw an error if the user ID does not exist', () => { - mockIdentitiesDataCollection.get.withArgs('doge').rejects(new Error('such error')); - return connection.testIdentity('doge', 'password') - .should.be.rejectedWith(/such error/); + it('should throw if the secret does not match', () => { + sinon.stub(connection, 'getIdentity').resolves(identity); + return connection.testIdentity('bob1', 'blahblah') + .should.be.rejectedWith(/The secret blahblah specified for the identity bob1 does not match the stored secret suchsecret/); }); - it('should throw an error if the user secret does not match', () => { - mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'not correct' }); - return connection.testIdentity('doge', 'password') - .should.be.rejectedWith(/ does not match/); + it('should not throw if the secret does match', () => { + sinon.stub(connection, 'getIdentity').resolves(identity); + return connection.testIdentity('bob1', 'suchsecret') + .should.eventually.be.equal(identity); }); }); @@ -365,6 +373,7 @@ describe('EmbeddedConnection', () => { mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'password' }); return connection.createIdentity(mockSecurityContext, 'doge') .then((result) => { + sinon.assert.notCalled(mockIdentitiesDataCollection.add); result.should.be.deep.equal({ userID: 'doge', userSecret: 'password' }); }); }); @@ -375,6 +384,14 @@ describe('EmbeddedConnection', () => { mockIdentitiesDataCollection.add.withArgs('doge').resolves(); return connection.createIdentity(mockSecurityContext, 'doge') .then((result) => { + sinon.assert.calledOnce(mockIdentitiesDataCollection.add); + sinon.assert.calledWith(mockIdentitiesDataCollection.add, 'doge', { + certificate: '', + identifier: '8f00d1b8319abc0ad87ccb6c1baae0a54c406c921c01e1ed165c33b93f3e5b6a', + issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e', + name: 'doge', + secret: 'f892c30a' + }); result.should.be.deep.equal({ userID: 'doge', userSecret: 'f892c30a' }); }); }); diff --git a/packages/composer-connector-embedded/test/embeddedsecuritycontext.js b/packages/composer-connector-embedded/test/embeddedsecuritycontext.js index b89899e4d0..83a0b704df 100644 --- a/packages/composer-connector-embedded/test/embeddedsecuritycontext.js +++ b/packages/composer-connector-embedded/test/embeddedsecuritycontext.js @@ -23,27 +23,35 @@ const sinon = require('sinon'); describe('EmbeddedSecurityContext', () => { + const identity = { + identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a', + name: 'bob1', + issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665', + secret: 'suchsecret', + certificate: '' + }; + let mockConnection; + let securityContext; beforeEach(() => { mockConnection = sinon.createStubInstance(Connection); + securityContext = new EmbeddedSecurityContext(mockConnection, identity); }); describe('#constructor', () => { it('should construct a new security context', () => { - let securityContext = new EmbeddedSecurityContext(mockConnection, 'bob1'); securityContext.should.be.an.instanceOf(SecurityContext); - securityContext.userID.should.equal('bob1'); + securityContext.identity.should.equal(identity); }); }); - describe('#getUserID', () => { + describe('#getIdentity', () => { it('should get the current user ID', () => { - let securityContext = new EmbeddedSecurityContext(mockConnection, 'bob1'); - securityContext.getUserID().should.equal('bob1'); + securityContext.getIdentity().should.deep.equal(identity); }); }); @@ -51,7 +59,6 @@ describe('EmbeddedSecurityContext', () => { describe('#getChaincodeID', () => { it('should get the chaincode ID', () => { - let securityContext = new EmbeddedSecurityContext(mockConnection, 'bob1'); securityContext.chaincodeID = 'ed916d6a-21af-4a2a-a9be-a86f69aa641b'; securityContext.getChaincodeID().should.equal('ed916d6a-21af-4a2a-a9be-a86f69aa641b'); }); @@ -61,7 +68,6 @@ describe('EmbeddedSecurityContext', () => { describe('#setChaincodeID', () => { it('should set the chaincode ID', () => { - let securityContext = new EmbeddedSecurityContext(mockConnection, 'bob1'); securityContext.setChaincodeID('ed916d6a-21af-4a2a-a9be-a86f69aa641b'); securityContext.chaincodeID.should.equal('ed916d6a-21af-4a2a-a9be-a86f69aa641b'); }); diff --git a/packages/composer-connector-web/lib/webconnection.js b/packages/composer-connector-web/lib/webconnection.js index c62debdf06..86c241f936 100644 --- a/packages/composer-connector-web/lib/webconnection.js +++ b/packages/composer-connector-web/lib/webconnection.js @@ -14,8 +14,8 @@ 'use strict'; -// const Resource = require('composer-common').Resource; const Connection = require('composer-common').Connection; +const createHash = require('sha.js'); const Engine = require('composer-runtime').Engine; const uuid = require('uuid'); const WebContainer = require('composer-runtime-web').WebContainer; @@ -29,6 +29,9 @@ const businessNetworks = {}; // A mapping of chaincode IDs to their instance objects. const chaincodes = {}; +// The issuer for all identities. +const DEFAULT_ISSUER = createHash('sha256').update('org1').digest('hex'); + /** * Base class representing a connection to a business network. * @protected @@ -150,30 +153,30 @@ class WebConnection extends Connection { * object representing the logged in participant, or rejected with a login error. */ login(enrollmentID, enrollmentSecret) { - // The 'admin' ID is special for the moment as it is not bound to a participant. - let result = new WebSecurityContext(this, enrollmentID !== 'admin' ? enrollmentID : null); if (!this.businessNetworkIdentifier) { return this.testIdentity(enrollmentID, enrollmentSecret) - .then(() => { - return result; + .then((identity) => { + return new WebSecurityContext(this, identity); }); } + let identity; return this.testIdentity(enrollmentID, enrollmentSecret) - .then(() => { + .then((identity_) => { + identity = identity_; return this.getChaincodeID(this.businessNetworkIdentifier); }) .then((chaincodeID) => { - if (chaincodeID) { - if (!WebConnection.getBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile)) { - let container = WebConnection.createContainer(chaincodeID); - let engine = WebConnection.createEngine(container); - WebConnection.addBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile, chaincodeID); - WebConnection.addChaincode(chaincodeID, container, engine); - } - result.setChaincodeID(chaincodeID); - } else { + if (!chaincodeID) { throw new Error(`No chaincode ID found for business network '${this.businessNetworkIdentifier}'`); } + if (!WebConnection.getBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile)) { + let container = WebConnection.createContainer(chaincodeID); + let engine = WebConnection.createEngine(container); + WebConnection.addBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile, chaincodeID); + WebConnection.addChaincode(chaincodeID, container, engine); + } + const result = new WebSecurityContext(this, identity); + result.setChaincodeID(chaincodeID); return result; }); } @@ -188,12 +191,12 @@ class WebConnection extends Connection { */ deploy(securityContext, businessNetwork, deployOptions) { let container = WebConnection.createContainer(); - let userID = securityContext.getUserID(); + let identity = securityContext.getIdentity(); let chaincodeID = container.getUUID(); let engine = WebConnection.createEngine(container); WebConnection.addBusinessNetwork(businessNetwork.getName(), this.connectionProfile, chaincodeID); WebConnection.addChaincode(chaincodeID, container, engine); - let context = new WebContext(engine, userID, this); + let context = new WebContext(engine, identity, this); return businessNetwork.toArchive() .then((businessNetworkArchive) => { const initArgs = {}; @@ -258,10 +261,10 @@ class WebConnection extends Connection { * chaincode function once it has been invoked, or rejected with an error. */ queryChainCode(securityContext, functionName, args) { - let userID = securityContext.getUserID(); + let identity = securityContext.getIdentity(); let chaincodeID = securityContext.getChaincodeID(); let chaincode = WebConnection.getChaincode(chaincodeID); - let context = new WebContext(chaincode.engine, userID, this); + let context = new WebContext(chaincode.engine, identity, this); return chaincode.engine.query(context, functionName, args) .then((data) => { return Buffer.from(JSON.stringify(data)); @@ -277,10 +280,10 @@ class WebConnection extends Connection { * has been invoked, or rejected with an error. */ invokeChainCode(securityContext, functionName, args) { - let userID = securityContext.getUserID(); + let identity = securityContext.getIdentity(); let chaincodeID = securityContext.getChaincodeID(); let chaincode = WebConnection.getChaincode(chaincodeID); - let context = new WebContext(chaincode.engine, userID, this); + let context = new WebContext(chaincode.engine, identity, this); return chaincode.engine.invoke(context, functionName, args) .then((data) => { return undefined; @@ -289,47 +292,57 @@ class WebConnection extends Connection { /** * Get the data collection that stores identities. - * @return {DataCollection} The data collection that stores identities. + * @return {Promise} A promise that is resolved with the data collection + * that stores identities. */ getIdentities() { - return this.dataService.existsCollection('identities') - .then((exists) => { - if (exists) { - return this.dataService.getCollection('identities'); - } else { - return this.dataService.createCollection('identities'); - } - }); + return this.dataService.ensureCollection('identities'); } /** - * Test the specified user ID and secret to ensure that it is valid. - * @param {string} userID The user ID. - * @param {string} userSecret The user secret. - * @return {Promise} A promise that is resolved if the user ID and secret - * is valid, or rejected with an error. + * Get the identity for the specified name. + * @param {string} identityName The name for the identity. + * @return {Promise} A promise that is resolved with the identity, or + * rejected with an error. */ - testIdentity(userID, userSecret) { - // The 'admin' ID is special for the moment as it is not bound to a participant. - if (userID === 'admin') { - return Promise.resolve(); + getIdentity(identityName) { + if (identityName === 'admin') { + return Promise.resolve({ + identifier: '', + name: 'admin', + issuer: DEFAULT_ISSUER, + secret: 'adminpw' + }); } return this.getIdentities() .then((identities) => { - return identities.get(userID); - }) + return identities.get(identityName); + }); + } + + /** + * Test the specified identity name and secret to ensure that it is valid. + * @param {string} identityName The name for the identity. + * @param {string} identitySecret The secret for the identity. + * @return {Promise} A promise that is resolved if the user ID and secret + * is valid, or rejected with an error. + */ + testIdentity(identityName, identitySecret) { + return this.getIdentity(identityName) .then((identity) => { - if (identity.userSecret !== userSecret) { - throw new Error(`The user secret ${userSecret} specified for the user ID ${userID} does not match the stored user secret ${identity.userSecret}`); + if (identityName !== 'admin') { + if (identity.secret !== identitySecret) { + throw new Error(`The secret ${identitySecret} specified for the identity ${identityName} does not match the stored secret ${identity.secret}`); + } } + return identity; }); - } /** - * Create a new identity for the specified user ID. + * Create a new identity for the specified name. * @param {SecurityContext} securityContext The participant's security context. - * @param {string} userID The user ID. + * @param {string} identityName The name for the new identity. * @param {object} [options] Options for the new identity. * @param {boolean} [options.issuer] Whether or not the new identity should have * permissions to create additional new identities. False by default. @@ -338,22 +351,32 @@ class WebConnection extends Connection { * @return {Promise} A promise that is resolved with a generated user * secret once the new identity has been created, or rejected with an error. */ - createIdentity(securityContext, userID, options) { + createIdentity(securityContext, identityName, options) { let identities; return this.getIdentities() .then((identities_) => { identities = identities_; - return identities.exists(userID); + return identities.exists(identityName); }) .then((exists) => { if (exists) { - return identities.get(userID); + return identities.get(identityName); } - const userSecret = uuid.v4().substring(0, 8); - const identity = { userID: userID, userSecret: userSecret }; - return identities.add(userID, identity) + const identifier = createHash('sha256').update(uuid.v4()).digest('hex'); + const secret = uuid.v4().substring(0, 8); + const identity = { + identifier, + name: identityName, + issuer: DEFAULT_ISSUER, + secret, + certificate: '' + }; + return identities.add(identityName, identity) .then(() => { - return identity; + return { + userID: identity.name, + userSecret: identity.secret + }; }); }); } diff --git a/packages/composer-connector-web/lib/websecuritycontext.js b/packages/composer-connector-web/lib/websecuritycontext.js index 9681ede74b..e67195fe94 100644 --- a/packages/composer-connector-web/lib/websecuritycontext.js +++ b/packages/composer-connector-web/lib/websecuritycontext.js @@ -24,20 +24,20 @@ class WebSecurityContext extends SecurityContext { /** * Constructor. * @param {Connection} connection The owning connection. - * @param {String} userID The current user ID. + * @param {Object} identity The current identity. */ - constructor(connection, userID) { + constructor(connection, identity) { super(connection); - this.userID = userID; + this.identity = identity; this.chaincodeID = null; } /** - * Get the current user ID. - * @return {string} The current user ID. + * Get the current identity. + * @return {string} The current identity. */ - getUserID() { - return this.userID; + getIdentity() { + return this.identity; } /** diff --git a/packages/composer-connector-web/test/webconnection.js b/packages/composer-connector-web/test/webconnection.js index 1b1eb11dae..f1ffd4f9a2 100644 --- a/packages/composer-connector-web/test/webconnection.js +++ b/packages/composer-connector-web/test/webconnection.js @@ -37,6 +37,14 @@ require('sinon-as-promised'); describe('WebConnection', () => { + const identity = { + identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a', + name: 'bob1', + issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665', + secret: 'suchsecret', + certificate: '' + }; + let sandbox; let mockConnectionManager; let mockConnectionProfileManager; @@ -107,25 +115,13 @@ describe('WebConnection', () => { sandbox.stub(connection, 'testIdentity').resolves(); }); - it('should return a new security context with a null user ID if the admin ID is specified', () => { - connection = new WebConnection(mockConnectionManager, 'devFabric1'); - sandbox.stub(connection, 'testIdentity').resolves(); - return connection.login('admin', 'suchs3cret') - .then((securityContext) => { - securityContext.should.be.an.instanceOf(WebSecurityContext); - should.equal(securityContext.getUserID(), null); - should.equal(securityContext.getChaincodeID(), null); - sinon.assert.calledWith(connection.testIdentity, 'admin', 'suchs3cret'); - }); - }); - it('should return a new security context with a null chaincode ID if the business network was not specified', () => { connection = new WebConnection(mockConnectionManager, 'devFabric1'); - sandbox.stub(connection, 'testIdentity').resolves(); + sandbox.stub(connection, 'testIdentity').resolves(identity); return connection.login('doge', 'suchs3cret') .then((securityContext) => { securityContext.should.be.an.instanceOf(WebSecurityContext); - securityContext.getUserID().should.equal('doge'); + securityContext.getIdentity().should.deep.equal(identity); should.equal(securityContext.getChaincodeID(), null); sinon.assert.calledWith(connection.testIdentity, 'doge', 'suchs3cret'); }); @@ -175,7 +171,7 @@ describe('WebConnection', () => { mockBusinessNetwork.getName.returns('testnetwork'); let mockContainer = sinon.createStubInstance(WebContainer); mockContainer.getUUID.returns('133c00a3-8555-4aa5-9165-9de9a8f8a838'); - mockSecurityContext.getUserID.returns('bob1'); + mockSecurityContext.getIdentity.returns(identity); sandbox.stub(WebConnection, 'createContainer').returns(mockContainer); let mockEngine = sinon.createStubInstance(Engine); mockEngine.getContainer.returns(mockContainer); @@ -187,7 +183,7 @@ describe('WebConnection', () => { sinon.assert.calledOnce(mockEngine.init); sinon.assert.calledWith(mockEngine.init, sinon.match((context) => { context.should.be.an.instanceOf(Context); - context.getIdentityService().getCurrentUserID().should.equal('bob1'); + context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); return true; }), 'init', ['aGVsbG8gd29ybGQ=', '{}']); sinon.assert.calledOnce(connection.ping); @@ -277,7 +273,7 @@ describe('WebConnection', () => { mockEngine.getContainer.returns(mockContainer); WebConnection.addBusinessNetwork('org.acme.Business', 'devFabric1', '6eeb8858-eced-4a32-b1cd-2491f1e3718f'); WebConnection.addChaincode('6eeb8858-eced-4a32-b1cd-2491f1e3718f', mockContainer, mockEngine); - mockSecurityContext.getUserID.returns('bob1'); + mockSecurityContext.getIdentity.returns(identity); mockSecurityContext.getChaincodeID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f'); mockEngine.query.resolves({ test: 'data from engine' }); return connection.queryChainCode(mockSecurityContext, 'testFunction', ['arg1', 'arg2']) @@ -285,7 +281,7 @@ describe('WebConnection', () => { sinon.assert.calledOnce(mockEngine.query); sinon.assert.calledWith(mockEngine.query, sinon.match((context) => { context.should.be.an.instanceOf(Context); - context.getIdentityService().getCurrentUserID().should.equal('bob1'); + context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); return true; }), 'testFunction', ['arg1', 'arg2']); result.should.be.an.instanceOf(Buffer); @@ -303,7 +299,7 @@ describe('WebConnection', () => { mockEngine.getContainer.returns(mockContainer); WebConnection.addBusinessNetwork('org.acme.Business', 'devFabric1', '6eeb8858-eced-4a32-b1cd-2491f1e3718f'); WebConnection.addChaincode('6eeb8858-eced-4a32-b1cd-2491f1e3718f', mockContainer, mockEngine); - mockSecurityContext.getUserID.returns('bob1'); + mockSecurityContext.getIdentity.returns(identity); mockSecurityContext.getChaincodeID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f'); mockEngine.invoke.resolves({ test: 'data from engine' }); return connection.invokeChainCode(mockSecurityContext, 'testFunction', ['arg1', 'arg2']) @@ -311,7 +307,7 @@ describe('WebConnection', () => { sinon.assert.calledOnce(mockEngine.invoke); sinon.assert.calledWith(mockEngine.invoke, sinon.match((context) => { context.should.be.an.instanceOf(Context); - context.getIdentityService().getCurrentUserID().should.equal('bob1'); + context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); return true; }), 'testFunction', ['arg1', 'arg2']); should.equal(result, undefined); @@ -328,57 +324,69 @@ describe('WebConnection', () => { beforeEach(() => { connection.dataService = mockDataService = sinon.createStubInstance(DataService); mockIdentitiesDataCollection = sinon.createStubInstance(DataCollection); - - }); - - it('should create and return the identities collection if it does not exist', () => { - mockDataService.existsCollection.withArgs('identities').resolves(false); - mockDataService.createCollection.withArgs('identities').resolves(mockIdentitiesDataCollection); - return connection.getIdentities() - .then((identities) => { - identities.should.equal(mockIdentitiesDataCollection); - }); }); - it('should return the existing identities collection if it already exists', () => { - mockDataService.existsCollection.withArgs('identities').resolves(true); - mockDataService.getCollection.withArgs('identities').resolves(mockIdentitiesDataCollection); + it('should ensure and return the identities collection', () => { + mockDataService.ensureCollection.withArgs('identities').resolves(mockIdentitiesDataCollection); return connection.getIdentities() - .then((identities) => { - identities.should.equal(mockIdentitiesDataCollection); - }); + .then((identities) => { + identities.should.equal(mockIdentitiesDataCollection); + }); }); }); - describe('#testIdentity', () => { + describe('#getIdentity', () => { let mockIdentitiesDataCollection; beforeEach(() => { mockIdentitiesDataCollection = sinon.createStubInstance(DataCollection); - sandbox.stub(connection, 'getIdentities').resolves(mockIdentitiesDataCollection); + sinon.stub(connection, 'getIdentities').resolves(mockIdentitiesDataCollection); }); - it('should resolve if the user ID is admin', () => { - return connection.testIdentity('admin', 'password'); + it('should return the hardcoded admin identity', () => { + return connection.getIdentity('admin') + .should.eventually.be.deep.equal({ + identifier: '', + name: 'admin', + issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e', + secret: 'adminpw' + }); }); - it('should resolve if the user ID exists', () => { - mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'password' }); - return connection.testIdentity('doge', 'password'); + it('should return the specified identity', () => { + mockIdentitiesDataCollection.get.withArgs('bob1').resolves(identity); + return connection.getIdentity('bob1') + .should.eventually.be.equal(identity); }); - it('should throw an error if the user ID does not exist', () => { - mockIdentitiesDataCollection.get.withArgs('doge').rejects(new Error('such error')); - return connection.testIdentity('doge', 'password') - .should.be.rejectedWith(/such error/); + }); + + describe('#testIdentity', () => { + + it('should not check the secret if the name is admin', () => { + const identity = { + identifier: '', + name: 'admin', + issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e', + secret: 'adminpw' + }; + sinon.stub(connection, 'getIdentity').resolves(identity); + return connection.testIdentity('admin', 'blahblah') + .should.eventually.be.equal(identity); }); - it('should throw an error if the user secret does not match', () => { - mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'not correct' }); - return connection.testIdentity('doge', 'password') - .should.be.rejectedWith(/ does not match/); + it('should throw if the secret does not match', () => { + sinon.stub(connection, 'getIdentity').resolves(identity); + return connection.testIdentity('bob1', 'blahblah') + .should.be.rejectedWith(/The secret blahblah specified for the identity bob1 does not match the stored secret suchsecret/); + }); + + it('should not throw if the secret does match', () => { + sinon.stub(connection, 'getIdentity').resolves(identity); + return connection.testIdentity('bob1', 'suchsecret') + .should.eventually.be.equal(identity); }); }); @@ -397,6 +405,7 @@ describe('WebConnection', () => { mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'password' }); return connection.createIdentity(mockSecurityContext, 'doge') .then((result) => { + sinon.assert.notCalled(mockIdentitiesDataCollection.add); result.should.be.deep.equal({ userID: 'doge', userSecret: 'password' }); }); }); @@ -407,6 +416,14 @@ describe('WebConnection', () => { mockIdentitiesDataCollection.add.withArgs('doge').resolves(); return connection.createIdentity(mockSecurityContext, 'doge') .then((result) => { + sinon.assert.calledOnce(mockIdentitiesDataCollection.add); + sinon.assert.calledWith(mockIdentitiesDataCollection.add, 'doge', { + certificate: '', + identifier: '8f00d1b8319abc0ad87ccb6c1baae0a54c406c921c01e1ed165c33b93f3e5b6a', + issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e', + name: 'doge', + secret: 'f892c30a' + }); result.should.be.deep.equal({ userID: 'doge', userSecret: 'f892c30a' }); }); }); diff --git a/packages/composer-connector-web/test/websecuritycontext.js b/packages/composer-connector-web/test/websecuritycontext.js index e08155c7d9..68c6256c50 100644 --- a/packages/composer-connector-web/test/websecuritycontext.js +++ b/packages/composer-connector-web/test/websecuritycontext.js @@ -23,27 +23,35 @@ const sinon = require('sinon'); describe('WebSecurityContext', () => { + const identity = { + identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a', + name: 'bob1', + issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665', + secret: 'suchsecret', + certificate: '' + }; + let mockConnection; + let securityContext; beforeEach(() => { mockConnection = sinon.createStubInstance(Connection); + securityContext = new WebSecurityContext(mockConnection, identity); }); describe('#constructor', () => { it('should construct a new security context', () => { - let securityContext = new WebSecurityContext(mockConnection, 'bob1'); securityContext.should.be.an.instanceOf(SecurityContext); - securityContext.userID.should.equal('bob1'); + securityContext.identity.should.equal(identity); }); }); - describe('#getUserID', () => { + describe('#getIdentity', () => { it('should get the current user ID', () => { - let securityContext = new WebSecurityContext(mockConnection, 'bob1'); - securityContext.getUserID().should.equal('bob1'); + securityContext.getIdentity().should.deep.equal(identity); }); }); @@ -51,7 +59,6 @@ describe('WebSecurityContext', () => { describe('#getChaincodeID', () => { it('should get the chaincode ID', () => { - let securityContext = new WebSecurityContext(mockConnection, 'bob1'); securityContext.chaincodeID = 'ed916d6a-21af-4a2a-a9be-a86f69aa641b'; securityContext.getChaincodeID().should.equal('ed916d6a-21af-4a2a-a9be-a86f69aa641b'); }); @@ -61,7 +68,6 @@ describe('WebSecurityContext', () => { describe('#setChaincodeID', () => { it('should set the chaincode ID', () => { - let securityContext = new WebSecurityContext(mockConnection, 'bob1'); securityContext.setChaincodeID('ed916d6a-21af-4a2a-a9be-a86f69aa641b'); securityContext.chaincodeID.should.equal('ed916d6a-21af-4a2a-a9be-a86f69aa641b'); }); diff --git a/packages/composer-runtime-embedded/lib/embeddedcontext.js b/packages/composer-runtime-embedded/lib/embeddedcontext.js index 12347cd70e..d690148a1e 100644 --- a/packages/composer-runtime-embedded/lib/embeddedcontext.js +++ b/packages/composer-runtime-embedded/lib/embeddedcontext.js @@ -30,13 +30,13 @@ class EmbeddedContext extends Context { /** * Constructor. * @param {Engine} engine The owning engine. - * @param {String} userID The current user ID. + * @param {Object} identity The current identity. * @param {EventEmitter} eventSink The event emitter */ - constructor(engine, userID, eventSink) { + constructor(engine, identity, eventSink) { super(engine); this.dataService = new EmbeddedDataService(engine.getContainer().getUUID()); - this.identityService = new EmbeddedIdentityService(userID); + this.identityService = new EmbeddedIdentityService(identity); this.eventSink = eventSink; } diff --git a/packages/composer-runtime-embedded/lib/embeddedidentityservice.js b/packages/composer-runtime-embedded/lib/embeddedidentityservice.js index 0cff6accca..b963d9e275 100644 --- a/packages/composer-runtime-embedded/lib/embeddedidentityservice.js +++ b/packages/composer-runtime-embedded/lib/embeddedidentityservice.js @@ -24,20 +24,43 @@ class EmbeddedIdentityService extends IdentityService { /** * Constructor. - * @param {String} userID The current user ID. + * @param {String} identity The current identity. */ - constructor(userID) { + constructor(identity) { super(); - this.userID = userID; + this.identity = identity; } /** - * Retrieve the current user ID. - * @return {string} The current user ID, or null if the current user ID cannot - * be determined or has not been specified. + * Get a unique identifier for the identity used to submit the transaction. + * @return {string} A unique identifier for the identity used to submit the transaction. */ - getCurrentUserID() { - return this.userID; + getIdentifier() { + return this.identity.identifier; + } + + /** + * Get the name of the identity used to submit the transaction. + * @return {string} The name of the identity used to submit the transaction. + */ + getName() { + return this.identity.name; + } + + /** + * Get the issuer of the identity used to submit the transaction. + * @return {string} The issuer of the identity used to submit the transaction. + */ + getIssuer() { + return this.identity.issuer; + } + + /** + * Get the certificate for the identity used to submit the transaction. + * @return {string} The certificate for the identity used to submit the transaction. + */ + getCertificate() { + return this.identity.certificate; } } diff --git a/packages/composer-runtime-embedded/test/embeddedcontext.js b/packages/composer-runtime-embedded/test/embeddedcontext.js index 4a2081eda8..62663100e5 100644 --- a/packages/composer-runtime-embedded/test/embeddedcontext.js +++ b/packages/composer-runtime-embedded/test/embeddedcontext.js @@ -30,9 +30,18 @@ const sinon = require('sinon'); describe('EmbeddedContext', () => { + const identity = { + identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a', + name: 'bob1', + issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665', + secret: 'suchsecret', + certificate: '' + }; + let mockEmbeddedContainer; let mockSerializer; let mockEngine; + let context; beforeEach(() => { mockEmbeddedContainer = sinon.createStubInstance(EmbeddedContainer); @@ -40,12 +49,12 @@ describe('EmbeddedContext', () => { mockEngine = sinon.createStubInstance(Engine); mockEngine.getContainer.returns(mockEmbeddedContainer); mockSerializer = sinon.createStubInstance(Serializer); + context = new EmbeddedContext(mockEngine, identity); }); describe('#constructor', () => { it('should construct a new context', () => { - let context = new EmbeddedContext(mockEngine, 'bob1'); context.should.be.an.instanceOf(Context); }); @@ -54,7 +63,6 @@ describe('EmbeddedContext', () => { describe('#getDataService', () => { it('should return the container data service', () => { - let context = new EmbeddedContext(mockEngine, 'bob1'); context.getDataService().should.be.an.instanceOf(EmbeddedDataService); }); @@ -63,9 +71,8 @@ describe('EmbeddedContext', () => { describe('#getIdentityService', () => { it('should return the container identity service', () => { - let context = new EmbeddedContext(mockEngine, 'bob1'); context.getIdentityService().should.be.an.instanceOf(EmbeddedIdentityService); - context.getIdentityService().getCurrentUserID().should.equal('bob1'); + context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); }); }); @@ -73,14 +80,12 @@ describe('EmbeddedContext', () => { describe('#getEventService', () => { it('should return the container event service', () => { - let context = new EmbeddedContext(mockEngine, 'bob1'); context.getSerializer = sinon.stub().returns(mockSerializer); context.getEventService().should.be.an.instanceOf(EmbeddedEventService); }); it('should return the container event service if it is set', () => { const mockEmbeddedEventService = sinon.createStubInstance(EmbeddedEventService); - let context = new EmbeddedContext(mockEngine, 'bob1'); context.eventService = mockEmbeddedEventService; context.getEventService().should.equal(mockEmbeddedEventService); }); @@ -89,13 +94,11 @@ describe('EmbeddedContext', () => { describe('#getHTTPService', () => { it('should return the container HTTP service', () => { - let context = new EmbeddedContext(mockEngine, 'bob1'); context.getHTTPService().should.be.an.instanceOf(EmbeddedHTTPService); }); it('should return the container HTTP service if it is set', () => { const mockEmbeddedHTTPService = sinon.createStubInstance(EmbeddedHTTPService); - let context = new EmbeddedContext(mockEngine, 'bob1'); context.httpService = mockEmbeddedHTTPService; context.getHTTPService().should.equal(mockEmbeddedHTTPService); }); @@ -104,13 +107,11 @@ describe('EmbeddedContext', () => { describe('#getScriptCompiler', () => { it('should return the container script compiler', () => { - let context = new EmbeddedContext(mockEngine, 'bob1'); context.getScriptCompiler().should.be.an.instanceOf(EmbeddedScriptCompiler); }); it('should return the container script compiler if it is set', () => { const mockEmbeddedScriptCompiler = sinon.createStubInstance(EmbeddedScriptCompiler); - const context = new EmbeddedContext(mockEngine, 'bob1'); context.scriptCompiler = mockEmbeddedScriptCompiler; context.getScriptCompiler().should.equal(mockEmbeddedScriptCompiler); }); diff --git a/packages/composer-runtime-embedded/test/embeddedidentityservice.js b/packages/composer-runtime-embedded/test/embeddedidentityservice.js index 6476eaeb3f..37cc3ef33a 100644 --- a/packages/composer-runtime-embedded/test/embeddedidentityservice.js +++ b/packages/composer-runtime-embedded/test/embeddedidentityservice.js @@ -22,11 +22,19 @@ const sinon = require('sinon'); describe('EmbeddedIdentityService', () => { + const identity = { + identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a', + name: 'bob1', + issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665', + secret: 'suchsecret', + certificate: '' + }; + let identityService; let sandbox; beforeEach(() => { - identityService = new EmbeddedIdentityService('bob1'); + identityService = new EmbeddedIdentityService(identity); sandbox = sinon.sandbox.create(); }); @@ -38,15 +46,48 @@ describe('EmbeddedIdentityService', () => { it('should create a identity service', () => { identityService.should.be.an.instanceOf(IdentityService); - identityService.userID.should.equal('bob1'); + identityService.identity.should.equal(identity); + }); + + }); + + describe('#constructor', () => { + + it('should create a identity service', () => { + identityService.should.be.an.instanceOf(IdentityService); + identityService.identity.should.equal(identity); + }); + + }); + + describe('#getIdentifier', () => { + + it('should return the identifier', () => { + should.equal(identityService.getIdentifier(), 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); + }); + + }); + + describe('#getName', () => { + + it('should return the name', () => { + should.equal(identityService.getName(), 'bob1'); + }); + + }); + + describe('#getIssuer', () => { + + it('should return the issuer', () => { + should.equal(identityService.getIssuer(), 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665'); }); }); - describe('#getCurrentUserID', () => { + describe('#getCertificate', () => { - it('should return the current user ID', () => { - should.equal(identityService.getCurrentUserID(), 'bob1'); + it('should return the certificate', () => { + should.equal(identityService.getCertificate(), ''); }); }); diff --git a/packages/composer-runtime-web/lib/webcontext.js b/packages/composer-runtime-web/lib/webcontext.js index eac1fca501..9af70f645c 100644 --- a/packages/composer-runtime-web/lib/webcontext.js +++ b/packages/composer-runtime-web/lib/webcontext.js @@ -29,13 +29,13 @@ class WebContext extends Context { /** * Constructor. * @param {Engine} engine The owning engine. - * @param {String} userID The current user ID. + * @param {Object} identity The current identity. * @param {EventEmitter} eventSink The event emitter */ - constructor(engine, userID, eventSink) { + constructor(engine, identity, eventSink) { super(engine); this.dataService = new WebDataService(engine.getContainer().getUUID()); - this.identityService = new WebIdentityService(userID); + this.identityService = new WebIdentityService(identity); this.eventSink = eventSink; } diff --git a/packages/composer-runtime-web/lib/webidentityservice.js b/packages/composer-runtime-web/lib/webidentityservice.js index 3983081ba6..5295c309a9 100644 --- a/packages/composer-runtime-web/lib/webidentityservice.js +++ b/packages/composer-runtime-web/lib/webidentityservice.js @@ -24,20 +24,43 @@ class WebIdentityService extends IdentityService { /** * Constructor. - * @param {String} userID The current user ID. + * @param {String} identity The current identity. */ - constructor(userID) { + constructor(identity) { super(); - this.userID = userID; + this.identity = identity; } /** - * Retrieve the current user ID. - * @return {string} The current user ID, or null if the current user ID cannot - * be determined or has not been specified. + * Get a unique identifier for the identity used to submit the transaction. + * @return {string} A unique identifier for the identity used to submit the transaction. */ - getCurrentUserID() { - return this.userID; + getIdentifier() { + return this.identity.identifier; + } + + /** + * Get the name of the identity used to submit the transaction. + * @return {string} The name of the identity used to submit the transaction. + */ + getName() { + return this.identity.name; + } + + /** + * Get the issuer of the identity used to submit the transaction. + * @return {string} The issuer of the identity used to submit the transaction. + */ + getIssuer() { + return this.identity.issuer; + } + + /** + * Get the certificate for the identity used to submit the transaction. + * @return {string} The certificate for the identity used to submit the transaction. + */ + getCertificate() { + return this.identity.certificate; } } diff --git a/packages/composer-runtime-web/test/webcontext.js b/packages/composer-runtime-web/test/webcontext.js index 5b6da12773..62583aca5e 100644 --- a/packages/composer-runtime-web/test/webcontext.js +++ b/packages/composer-runtime-web/test/webcontext.js @@ -29,9 +29,18 @@ const sinon = require('sinon'); describe('WebContext', () => { + const identity = { + identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a', + name: 'bob1', + issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665', + secret: 'suchsecret', + certificate: '' + }; + let mockWebContainer; let mockSerializer; let mockEngine; + let context; beforeEach(() => { mockWebContainer = sinon.createStubInstance(WebContainer); @@ -39,12 +48,12 @@ describe('WebContext', () => { mockEngine = sinon.createStubInstance(Engine); mockEngine.getContainer.returns(mockWebContainer); mockSerializer = sinon.createStubInstance(Serializer); + context = new WebContext(mockEngine, identity); }); describe('#constructor', () => { it('should construct a new context', () => { - let context = new WebContext(mockEngine, 'bob1'); context.should.be.an.instanceOf(Context); }); @@ -53,7 +62,6 @@ describe('WebContext', () => { describe('#getDataService', () => { it('should return the container logging service', () => { - let context = new WebContext(mockEngine, 'bob1'); context.getDataService().should.be.an.instanceOf(WebDataService); }); @@ -62,9 +70,8 @@ describe('WebContext', () => { describe('#getIdentityService', () => { it('should return the container identity service', () => { - let context = new WebContext(mockEngine, 'bob1'); context.getIdentityService().should.be.an.instanceOf(WebIdentityService); - context.getIdentityService().getCurrentUserID().should.equal('bob1'); + context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); }); }); @@ -72,14 +79,12 @@ describe('WebContext', () => { describe('#getEventService', () => { it('should return the container event service', () => { - let context = new WebContext(mockEngine, 'bob1'); context.getSerializer = sinon.stub().returns(mockSerializer); context.getEventService().should.be.an.instanceOf(WebEventService); }); it('should return this.eventService if it is set', () => { const mockWebEventService = sinon.createStubInstance(WebEventService); - let context = new WebContext(mockEngine, 'bob1'); context.eventService = mockWebEventService; context.getEventService().should.equal(mockWebEventService); }); @@ -88,13 +93,11 @@ describe('WebContext', () => { describe('#getHTTPService', () => { it('should return the container http service', () => { - let context = new WebContext(mockEngine, 'bob1'); context.getHTTPService().should.be.an.instanceOf(WebHTTPService); }); it('should return this.httpService if it is set', () => { const mockWebHTTPService = sinon.createStubInstance(WebHTTPService); - let context = new WebContext(mockEngine, 'bob1'); context.httpService = mockWebHTTPService; context.getHTTPService().should.equal(mockWebHTTPService); }); diff --git a/packages/composer-runtime-web/test/webidentityservice.js b/packages/composer-runtime-web/test/webidentityservice.js index 5c861b8525..a314a48b4d 100644 --- a/packages/composer-runtime-web/test/webidentityservice.js +++ b/packages/composer-runtime-web/test/webidentityservice.js @@ -22,11 +22,19 @@ const sinon = require('sinon'); describe('WebIdentityService', () => { + const identity = { + identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a', + name: 'bob1', + issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665', + secret: 'suchsecret', + certificate: '' + }; + let identityService; let sandbox; beforeEach(() => { - identityService = new WebIdentityService('bob1'); + identityService = new WebIdentityService(identity); sandbox = sinon.sandbox.create(); }); @@ -38,15 +46,39 @@ describe('WebIdentityService', () => { it('should create a identity service', () => { identityService.should.be.an.instanceOf(IdentityService); - identityService.userID.should.equal('bob1'); + identityService.identity.should.equal(identity); + }); + + }); + + describe('#getIdentifier', () => { + + it('should return the identifier', () => { + should.equal(identityService.getIdentifier(), 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a'); + }); + + }); + + describe('#getName', () => { + + it('should return the name', () => { + should.equal(identityService.getName(), 'bob1'); + }); + + }); + + describe('#getIssuer', () => { + + it('should return the issuer', () => { + should.equal(identityService.getIssuer(), 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665'); }); }); - describe('#getCurrentUserID', () => { + describe('#getCertificate', () => { - it('should return the current user ID', () => { - should.equal(identityService.getCurrentUserID(), 'bob1'); + it('should return the certificate', () => { + should.equal(identityService.getCertificate(), ''); }); }); diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js index 3e8b264432..08f238dc09 100644 --- a/packages/composer-runtime/lib/context.js +++ b/packages/composer-runtime/lib/context.js @@ -105,7 +105,6 @@ class Context { this.api = null; this.queryExecutor = null; this.identityManager = null; - this.identity = null; this.participant = null; this.transaction = null; this.accessController = null; @@ -290,9 +289,6 @@ class Context { return this.getIdentityManager().getIdentity() .then((identity) => { - // Save the current identity. - this.identity = identity; - // Validate the identity. try { this.getIdentityManager().validateIdentity(identity); @@ -300,7 +296,12 @@ class Context { // Check for the case of activation required, and the user is trying to activate. if (e.activationRequired && this.getFunction() === 'activateIdentity') { - // Ignore. + + // Don't throw the error as we are activating the identity, but return null + // so that the participant is not set because there is no current participant + // until the identity is activated and not revoked. + return null; + } else { throw e; } diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js index 60344d2f5a..ac19778530 100644 --- a/packages/composer-runtime/test/context.js +++ b/packages/composer-runtime/test/context.js @@ -310,7 +310,7 @@ describe('Context', () => { error.activationRequired = true; mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(error); return context.loadCurrentParticipant() - .should.eventually.be.equal(mockParticipant) + .should.eventually.be.null .then(() => { sinon.assert.calledOnce(mockIdentityManager.validateIdentity); sinon.assert.calledWith(mockIdentityManager.validateIdentity, mockIdentity); From 99b50dfc520903ffd71736a7297eaf991445f8cf Mon Sep 17 00:00:00 2001 From: Simon Stone Date: Thu, 13 Jul 2017 08:18:07 +0100 Subject: [PATCH 3/7] Add composer identity bind command to CLI (#1547) --- .../lib/cmds/identity/bindCommand.js | 40 +++++ .../lib/cmds/identity/issueCommand.js | 2 +- .../lib/cmds/identity/lib/bind.js | 93 ++++++++++++ .../lib/cmds/identity/revokeCommand.js | 2 +- packages/composer-cli/package.json | 8 +- packages/composer-cli/test/identity/bind.js | 138 ++++++++++++++++++ packages/composer-runtime/lib/context.js | 10 +- packages/composer-runtime/test/context.js | 18 +++ 8 files changed, 301 insertions(+), 10 deletions(-) create mode 100644 packages/composer-cli/lib/cmds/identity/bindCommand.js create mode 100644 packages/composer-cli/lib/cmds/identity/lib/bind.js create mode 100644 packages/composer-cli/test/identity/bind.js diff --git a/packages/composer-cli/lib/cmds/identity/bindCommand.js b/packages/composer-cli/lib/cmds/identity/bindCommand.js new file mode 100644 index 0000000000..9ff7b29a08 --- /dev/null +++ b/packages/composer-cli/lib/cmds/identity/bindCommand.js @@ -0,0 +1,40 @@ +/* + * 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. + */ + +'use strict'; + +const Bind = require ('./lib/bind.js'); + +module.exports.command = 'bind [options]'; +module.exports.describe = 'Bind an existing identity to a participant in a participant registry'; +module.exports.builder = { + connectionProfileName: {alias: 'p', required: false, describe: 'The connection profile name', type: 'string' }, + businessNetworkName: {alias: 'n', required: true, describe: 'The business network name', type: 'string' }, + enrollId: { alias: 'i', required: true, describe: 'The enrollment ID of the user', type: 'string' }, + enrollSecret: { alias: 's', required: false, describe: 'The enrollment secret of the user', type: 'string' }, + participantId: { alias: 'a', required: true, describe: 'The particpant to issue the new identity to', type: 'string' }, + publicKeyFile: { alias: 'c', required: true, describe: 'File containing the public key', type: 'string' } +}; + +module.exports.handler = (argv) => { + + argv.thePromise = Bind.handler(argv) + .then(() => { + return; + }) + .catch((error) => { + throw error; + }); + return argv.thePromise; +}; diff --git a/packages/composer-cli/lib/cmds/identity/issueCommand.js b/packages/composer-cli/lib/cmds/identity/issueCommand.js index 181e09d590..f36a32bdb5 100644 --- a/packages/composer-cli/lib/cmds/identity/issueCommand.js +++ b/packages/composer-cli/lib/cmds/identity/issueCommand.js @@ -17,7 +17,7 @@ const Issue = require ('./lib/issue.js'); module.exports.command = 'issue [options]'; -module.exports.describe = 'Issue an identity to a participant in a participant registry'; +module.exports.describe = 'Issue a new identity to a participant in a participant registry'; module.exports.builder = { connectionProfileName: {alias: 'p', required: false, describe: 'The connection profile name', type: 'string' }, businessNetworkName: {alias: 'n', required: true, describe: 'The business network name', type: 'string' }, diff --git a/packages/composer-cli/lib/cmds/identity/lib/bind.js b/packages/composer-cli/lib/cmds/identity/lib/bind.js new file mode 100644 index 0000000000..701453b511 --- /dev/null +++ b/packages/composer-cli/lib/cmds/identity/lib/bind.js @@ -0,0 +1,93 @@ +/* + * 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. + */ + +'use strict'; + +const cmdUtil = require('../../utils/cmdutils'); +const DEFAULT_PROFILE_NAME = 'defaultProfile'; +const fs = require('fs'); + +/** + *

+ * Composer "identity bind" command + *

+ *

+ * @private + */ +class Bind { + + /** + * Command process for deploy command + * @param {string} argv argument list from composer command + * @return {Promise} promise when command complete + */ + static handler(argv) { + let businessNetworkConnection; + let enrollId; + let enrollSecret; + let connectionProfileName = Bind.getDefaultProfileName(argv); + let businessNetworkName; + let participantId = argv.participantId; + let publicKeyFile = argv.publicKeyFile; + let certificate; + try { + certificate = fs.readFileSync(publicKeyFile).toString(); + } catch(error) { + return Promise.reject(new Error('Unable to read public key file ' + publicKeyFile + '. ' + error.message)); + } + + return (() => { + if (!argv.enrollSecret) { + return cmdUtil.prompt({ + name: 'enrollmentSecret', + description: 'What is the enrollment secret of the user?', + required: true, + hidden: true, + replace: '*' + }) + .then((result) => { + argv.enrollSecret = result; + }); + } else { + return Promise.resolve(); + } + })() + .then(() => { + enrollId = argv.enrollId; + enrollSecret = argv.enrollSecret; + businessNetworkName = argv.businessNetworkName; + businessNetworkConnection = cmdUtil.createBusinessNetworkConnection(); + return businessNetworkConnection.connect(connectionProfileName, businessNetworkName, enrollId, enrollSecret); + }) + .then(() => { + return businessNetworkConnection.bindIdentity(participantId, certificate); + }) + .then((result) => { + console.log(`An identity was bound to the participant '${participantId}'`); + console.log('The participant can now connect to the business network using the identity'); + }); + } + + /** + * Get default profile name + * @param {argv} argv program arguments + * @return {String} defaultConnection profile name + */ + static getDefaultProfileName(argv) { + return argv.connectionProfileName || DEFAULT_PROFILE_NAME; + } + +} + +module.exports = Bind; diff --git a/packages/composer-cli/lib/cmds/identity/revokeCommand.js b/packages/composer-cli/lib/cmds/identity/revokeCommand.js index 6fdc8e9559..aa9239640d 100644 --- a/packages/composer-cli/lib/cmds/identity/revokeCommand.js +++ b/packages/composer-cli/lib/cmds/identity/revokeCommand.js @@ -17,7 +17,7 @@ const Revoke = require ('./lib/revoke.js'); module.exports.command = 'revoke [options]'; -module.exports.describe = 'Revoke an identity that was issued to a participant'; +module.exports.describe = 'Revoke an identity that was issued or bound to a participant'; module.exports.builder = { connectionProfileName: {alias: 'p', required: false, describe: 'The connection profile name', type: 'string' }, businessNetworkName: {alias: 'n', required: true, describe: 'The business network name', type: 'string' }, diff --git a/packages/composer-cli/package.json b/packages/composer-cli/package.json index 60efd18aa8..816736169c 100644 --- a/packages/composer-cli/package.json +++ b/packages/composer-cli/package.json @@ -83,9 +83,9 @@ ], "all": true, "check-coverage": true, - "statements": 60, - "branches": 50, - "functions": 58, - "lines": 60 + "statements": 70, + "branches": 56, + "functions": 62, + "lines": 70 } } diff --git a/packages/composer-cli/test/identity/bind.js b/packages/composer-cli/test/identity/bind.js new file mode 100644 index 0000000000..15641a0ed9 --- /dev/null +++ b/packages/composer-cli/test/identity/bind.js @@ -0,0 +1,138 @@ +/* + * 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. + */ + +'use strict'; + +const Client = require('composer-client'); +const BusinessNetworkConnection = Client.BusinessNetworkConnection; + +const Bind = require('../../lib/cmds/identity/bindCommand.js'); +const CmdUtil = require('../../lib/cmds/utils/cmdutils.js'); + +const sinon = require('sinon'); +require('sinon-as-promised'); +const chai = require('chai'); +chai.should(); +chai.use(require('chai-as-promised')); + +const fs = require('fs'); + +const BUSINESS_NETWORK_NAME = 'net.biz.TestNetwork-0.0.1'; +const DEFAULT_PROFILE_NAME = 'defaultProfile'; +const ENROLL_ID = 'SuccessKid'; +const ENROLL_SECRET = 'SuccessKidWin'; + +describe('composer identity bind CLI unit tests', () => { + + const pem = '-----BEGIN CERTIFICATE-----\nMIIB8TCCAZegAwIBAgIUKhzgF0nmP1Zl6uubdgq6wFohMC8wCgYIKoZIzj0EAwIw\nczELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh\nbiBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT\nE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNzEyMjIzNzAwWhcNMTgwNzEyMjIz\nNzAwWjAQMQ4wDAYDVQQDEwVhZG1pbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBPnG/X+v7R2ljdtRoreLnub9vlvU9BmF7LTuklTC0iHJr96ecQRkx6OCE8nDK09G\n5P6LvL95PUxlhDDdlStqASGjbDBqMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8E\nAjAAMB0GA1UdDgQWBBQl3QT+2qX5eTM2zaJ+MjU4vF+NQDArBgNVHSMEJDAigCAZ\nq2WruwSAfa0S5MCpqqZknnCGjjq9AhejItieR+GmrjAKBggqhkjOPQQDAgNIADBF\nAiEAok4RzwsOX7lSkUmGK+yfr9reJxvtyCHxQ68YC7blAQECIB3T3H0iwX+2MZaX\nmJucq33qkeHpFiq1focn3o6WAx/x\n-----END CERTIFICATE-----\n'; + + let sandbox; + let mockBusinessNetworkConnection; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockBusinessNetworkConnection = sinon.createStubInstance(BusinessNetworkConnection); + mockBusinessNetworkConnection.connect.resolves(); + mockBusinessNetworkConnection.bindIdentity.withArgs('org.doge.Doge#DOGE_1', pem, sinon.match.object).resolves({ + userID: 'dogeid1', + userSecret: 'suchsecret' + }); + sandbox.stub(fs, 'readFileSync').withArgs('admin.pem').returns(pem); + sandbox.stub(CmdUtil, 'createBusinessNetworkConnection').returns(mockBusinessNetworkConnection); + sandbox.stub(process, 'exit'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should bind an existing identity using the default profile', () => { + let argv = { + businessNetworkName: BUSINESS_NETWORK_NAME, + enrollId: ENROLL_ID, + enrollSecret: ENROLL_SECRET, + participantId: 'org.doge.Doge#DOGE_1', + publicKeyFile: 'admin.pem' + }; + return Bind.handler(argv) + .then((res) => { + sinon.assert.calledOnce(mockBusinessNetworkConnection.connect); + sinon.assert.calledWith(mockBusinessNetworkConnection.connect, DEFAULT_PROFILE_NAME, argv.businessNetworkName, argv.enrollId, argv.enrollSecret); + sinon.assert.calledOnce(mockBusinessNetworkConnection.bindIdentity); + sinon.assert.calledWith(mockBusinessNetworkConnection.bindIdentity, 'org.doge.Doge#DOGE_1', pem); + }); + }); + + it('should bind an existing identity using the specified profile', () => { + let argv = { + connectionProfileName: 'someOtherProfile', + businessNetworkName: BUSINESS_NETWORK_NAME, + enrollId: ENROLL_ID, + enrollSecret: ENROLL_SECRET, + participantId: 'org.doge.Doge#DOGE_1', + publicKeyFile: 'admin.pem' + }; + return Bind.handler(argv) + .then((res) => { + sinon.assert.calledOnce(mockBusinessNetworkConnection.connect); + sinon.assert.calledWith(mockBusinessNetworkConnection.connect, 'someOtherProfile', argv.businessNetworkName, argv.enrollId, argv.enrollSecret); + sinon.assert.calledOnce(mockBusinessNetworkConnection.bindIdentity); + sinon.assert.calledWith(mockBusinessNetworkConnection.bindIdentity, 'org.doge.Doge#DOGE_1', pem); + }); + }); + + it('should prompt for the enrollment secret if not specified', () => { + sandbox.stub(CmdUtil, 'prompt').resolves(ENROLL_SECRET); + let argv = { + businessNetworkName: BUSINESS_NETWORK_NAME, + enrollId: ENROLL_ID, + participantId: 'org.doge.Doge#DOGE_1', + publicKeyFile: 'admin.pem' + }; + return Bind.handler(argv) + .then((res) => { + sinon.assert.calledOnce(mockBusinessNetworkConnection.connect); + sinon.assert.calledWith(mockBusinessNetworkConnection.connect, DEFAULT_PROFILE_NAME, argv.businessNetworkName, argv.enrollId, argv.enrollSecret); + sinon.assert.calledOnce(mockBusinessNetworkConnection.bindIdentity); + sinon.assert.calledWith(mockBusinessNetworkConnection.bindIdentity, 'org.doge.Doge#DOGE_1', pem); + }); + }); + + it('should error when the certificate file cannot be read', () => { + fs.readFileSync.withArgs('admin.pem').throws(new Error('such error')); + let argv = { + businessNetworkName: BUSINESS_NETWORK_NAME, + enrollId: ENROLL_ID, + enrollSecret: ENROLL_SECRET, + participantId: 'org.doge.Doge#DOGE_1', + publicKeyFile: 'admin.pem' + }; + return Bind.handler(argv) + .should.be.rejectedWith(/such error/); + }); + + it('should error when the existing identity cannot be bound', () => { + mockBusinessNetworkConnection.bindIdentity.withArgs('org.doge.Doge#DOGE_1', pem).rejects(new Error('such error')); + let argv = { + businessNetworkName: BUSINESS_NETWORK_NAME, + enrollId: ENROLL_ID, + enrollSecret: ENROLL_SECRET, + participantId: 'org.doge.Doge#DOGE_1', + publicKeyFile: 'admin.pem' + }; + return Bind.handler(argv) + .should.be.rejectedWith(/such error/); + }); + +}); diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js index 08f238dc09..397dfca1dc 100644 --- a/packages/composer-runtime/lib/context.js +++ b/packages/composer-runtime/lib/context.js @@ -321,10 +321,12 @@ class Context { // Check for an admin user. // TODO: this is temporary whilst we migrate to requiring all // users to have identities that are mapped to participants. - const name = this.getIdentityService().getName(); - if (name && name.match(/admin/i)) { - LOG.exit(method, null); - return null; + if (!error.activationRequired) { + const name = this.getIdentityService().getName(); + if (name && name.match(/admin/i)) { + LOG.exit(method, null); + return null; + } } // Throw the error. diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js index ac19778530..ba8df7e850 100644 --- a/packages/composer-runtime/test/context.js +++ b/packages/composer-runtime/test/context.js @@ -336,6 +336,24 @@ describe('Context', () => { .should.be.rejectedWith(/such error/); }); + it('should not ignore an activation required error from looking up the identity for admin users', () => { + mockIdentityManager.getIdentity.resolves(mockIdentity); + mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant); + context.function = 'bindIdentity'; + const error = new Error('such error'); + error.activationRequired = true; + mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(error); + let promise = Promise.resolve(); + ['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => { + mockIdentityService.getName.returns(admin); + promise = promise.then(() => { + return context.loadCurrentParticipant() + .should.be.rejectedWith(/such error/); + }); + }); + return promise; + }); + it('should ignore any errors from looking up the identity for admin users', () => { let promise = Promise.resolve(); ['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => { From ecb1c6517b2f9bbd56818588b81b924c3c79b6cf Mon Sep 17 00:00:00 2001 From: Liam Grace <14gracel@users.noreply.github.com> Date: Thu, 13 Jul 2017 19:35:17 +0100 Subject: [PATCH 4/7] Identity registry APIs (#1553) * Remove double scroll bar (#1527) * Fix ability to issue identity to none existent participant (#1526) * Prevent identifying fields in transaction and event model (#1517) (#1544) * Fixed protractor tests * Prevent identifying fields in transaction and event model * Updated tutorial guide, minor fixes to cmdline , package.json instruction (#1534) * Updated Developer Tutorial removed version info and minor errata and tidy up * Updated Developer Tutorial package.json instruction * Use workaround for jekyll issue #6221 (#1551) * Merge PRs 1548 + 1550, pull e2e tests out into separate build (#1552) * comment out the exit * Move the PouchDB adapter plugin loading into the modules that need it * Pull out e2e tests into separate Travis build * Comment out e2e tests * Initial drop of identity registry (#1539) * Get identities working again on embedded and web runtimes (#1546) * Cannot activate issued identity as identity not found * Get identities working again on embedded and web runtimes * Add composer identity bind command to CLI (#1547) * Identity registry APIs --- .travis.yml | 1 + .travis/before-install.sh | 4 +- .travis/script.sh | 6 + .../data/businessnetwork/models/mozart.cto | 2 +- packages/composer-client/api.txt | 4 + packages/composer-client/changelog.txt | 3 +- .../lib/businessnetworkconnection.js | 47 +- .../composer-client/lib/identityregistry.js | 148 +++++ .../test/businessnetworkconnection.js | 95 ++++ .../composer-client/test/identityregistry.js | 193 +++++++ .../lib/introspect/classdeclaration.js | 11 + .../lib/introspect/eventdeclaration.js | 14 + .../lib/introspect/transactiondeclaration.js | 15 + packages/composer-common/lib/system.cto | 2 +- .../test/data/model/concept2.cto | 10 +- .../model/dependencies/contract/proforma.cto | 4 +- .../test/data/model/farm2fork.cto | 2 +- .../classdeclaration.dupetransactionname.cto | 4 +- .../test/introspect/functiondeclaration.js | 2 +- .../test/introspect/transactiondeclaration.js | 13 +- .../e2e/data/bna/basic-sample-network.bna | Bin 5574 -> 6444 bytes .../e2e/tests/editor-define.spec.ts | 507 +++++++++--------- .../e2e/utils/add-file-helper.ts | 23 +- .../e2e/utils/editor-helper.ts | 9 +- .../e2e/utils/import-helper.ts | 10 +- .../e2e/utils/operations-helper.ts | 2 +- .../e2e/utils/replace-helper.ts | 4 +- packages/composer-playground/package.json | 3 +- .../src/app/app.component.html | 4 +- .../src/app/footer/footer.component.scss | 5 +- .../styles/components/_side-bar-nav.scss | 2 +- .../lib/embeddeddataservice.js | 3 + .../composer-runtime-embedded/package.json | 1 + .../lib/pouchdbdataservice.js | 13 +- .../composer-runtime-pouchdb/package.json | 3 - .../test/pouchdbdataservice.js | 14 + .../lib/webdataservice.js | 4 + packages/composer-runtime-web/package.json | 2 + .../composer-runtime/test/data/mozart.cto | 2 +- .../composer-systests/systest/identities.js | 7 + .../jekylldocs/tutorials/developer-guide.md | 31 +- .../composer-website/scripts/setup-jekyll.sh | 2 +- .../templates/models/namespace.cto | 2 +- .../model/templates/models/namespace.cto | 2 +- .../test/businessnetworkconnector.js | 3 +- 45 files changed, 888 insertions(+), 350 deletions(-) create mode 100644 packages/composer-client/lib/identityregistry.js create mode 100644 packages/composer-client/test/identityregistry.js diff --git a/.travis.yml b/.travis.yml index 90031641c5..3d9853d422 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ matrix: - env: SYSTEST=embedded,proxy,web FC_TASK=systest - env: SYSTEST=hlf FC_TASK=systest - env: SYSTEST=hlfv1_tls FC_TASK=systest + # - env: SYSTEST=e2e FC_TASK=systest dist: trusty addons: apt: diff --git a/.travis/before-install.sh b/.travis/before-install.sh index bb79105b1d..b06ea73376 100755 --- a/.travis/before-install.sh +++ b/.travis/before-install.sh @@ -99,8 +99,8 @@ then if [ "${FC_TASK}" != "docs" ]; then # echo "ABORT_BUILD=true" > ${DIR}/build.cfg # echo "ABORT_CODE=0" >> ${DIR}/build.cfg - echo 'Docs only build - no pointing bothering with anything more' - exit 0 + echo 'Docs only build' +# exit 0 fi else diff --git a/.travis/script.sh b/.travis/script.sh index aaddc3d978..98b544cc79 100755 --- a/.travis/script.sh +++ b/.travis/script.sh @@ -45,6 +45,12 @@ if [ "${DOCS}" != "" ]; then npm run linkcheck:unstable fi +# Are we running playground e2e tests? +elif [ "${SYSTEST}" = "e2e" ]; then + + # Run the playground e2e tests. + cd "${DIR}/packages/composer-playground" + npm run e2e:main # Are we running system tests? elif [ "${SYSTEST}" != "" ]; then diff --git a/packages/composer-admin/test/data/businessnetwork/models/mozart.cto b/packages/composer-admin/test/data/businessnetwork/models/mozart.cto index 221437a2be..76e47aed01 100644 --- a/packages/composer-admin/test/data/businessnetwork/models/mozart.cto +++ b/packages/composer-admin/test/data/businessnetwork/models/mozart.cto @@ -103,7 +103,7 @@ asset Business identified by sbi { /** * An abstract transaction type for animal movements */ -abstract transaction AnimalMovement identified by transactionId { +abstract transaction AnimalMovement { o String transactionId o String[] logs optional --> Animal animal diff --git a/packages/composer-client/api.txt b/packages/composer-client/api.txt index 59b72cc7e8..c89670c5d7 100644 --- a/packages/composer-client/api.txt +++ b/packages/composer-client/api.txt @@ -17,6 +17,7 @@ class BusinessNetworkConnection extends EventEmitter { + Promise participantRegistryExists(string) + Promise addParticipantRegistry(string,string) + Promise getTransactionRegistry() + + Promise getIdentityRegistry() + Promise connect(string,string,string,string,Object) + Promise disconnect() + Promise submitTransaction(Resource) @@ -27,6 +28,9 @@ class BusinessNetworkConnection extends EventEmitter { + Promise bindIdentity(string) + Promise revokeIdentity(string) } +class IdentityRegistry extends Registry { + + Promise getIdentityRegistry(SecurityContext,ModelManager,Factory,Serializer) +} class ParticipantRegistry extends Registry { + Promise getAllParticipantRegistries(SecurityContext,ModelManager,Factory,Serializer) + Promise getParticipantRegistry(SecurityContext,string,ModelManager,Factory,Serializer) diff --git a/packages/composer-client/changelog.txt b/packages/composer-client/changelog.txt index 42108c1389..600b3f99b2 100644 --- a/packages/composer-client/changelog.txt +++ b/packages/composer-client/changelog.txt @@ -12,8 +12,9 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # -Version 0.9.2 {465d5a96640dce8240a392d058e06fe5} 2017-07-11 +Version 0.9.2 {60d41f6d3b545fd4ad53ca2bf0e5a36c} 2017-07-11 - Added bindIdentity +- Added getIdentityRegistry Version 0.9.0 {d6ebf2b722345ee0f7dac56f42730f12} 2017-06-26 - Added buildQuery and query diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js index bace44d451..51901864cb 100644 --- a/packages/composer-client/lib/businessnetworkconnection.js +++ b/packages/composer-client/lib/businessnetworkconnection.js @@ -28,6 +28,7 @@ const Query = require('./query'); const Resource = require('composer-common').Resource; const TransactionDeclaration = require('composer-common').TransactionDeclaration; const TransactionRegistry = require('./transactionregistry'); +const IdentityRegistry = require('./identityregistry'); const Util = require('composer-common').Util; const uuid = require('uuid'); @@ -288,6 +289,33 @@ class BusinessNetworkConnection extends EventEmitter { }); } + /** + * Get the identity registry. + * @example + * // Get the transaction registry + * var businessNetwork = new BusinessNetworkConnection(); + * return businessNetwork.connect('testprofile', 'businessNetworkIdentifier', 'WebAppAdmin', 'DJY27pEnl16d') + * .then(function(businessNetworkDefinition){ + * return businessNetworkDefinition.getIdentityRegistry(); + * }) + * .then(function(identityRegistry){ + * // Retrieved Identity Registry + * }); + * @return {Promise} - A promise that will be resolved to the {@link IdentityRegistry} + */ + getIdentityRegistry() { + Util.securityCheck(this.securityContext); + return IdentityRegistry + .getIdentityRegistry(this.securityContext, this.getBusinessNetwork().getModelManager(), this.getBusinessNetwork().getFactory(), this.getBusinessNetwork().getSerializer()) + .then((identityRegistry) => { + if (identityRegistry) { + return identityRegistry; + } else { + throw new Error('Failed to find the default identity registry'); + } + }); + } + /** * Connects to a business network using a connection profile, and authenticates to the Hyperledger Fabric. * @example @@ -600,13 +628,30 @@ class BusinessNetworkConnection extends EventEmitter { throw new Error('identityName not specified'); } let participantFQI; + let participantId; + let participantType; if (participant instanceof Resource) { participantFQI = participant.getFullyQualifiedIdentifier(); + participantId = participant.getIdentifier(); + participantType = participant.getFullyQualifiedType(); } else { participantFQI = participant; + participantId = participantFQI.substring(participantFQI.lastIndexOf('#') + 1); + participantType = participantFQI.substr(0, participantFQI.lastIndexOf('#')); } + Util.securityCheck(this.securityContext); - return this.connection.createIdentity(this.securityContext, identityName, options) + return this.getParticipantRegistry(participantType) + .then((participantRegistry) => { + return participantRegistry.exists(participantId); + }) + .then((exists) => { + if (exists) { + return this.connection.createIdentity(this.securityContext, identityName, options); + } else { + throw new Error(`Participant '${participantFQI}' does not exist `); + } + }) .then((identity) => { return Util.invokeChainCode(this.securityContext, 'issueIdentity', [participantFQI, identityName]) .then(() => { diff --git a/packages/composer-client/lib/identityregistry.js b/packages/composer-client/lib/identityregistry.js new file mode 100644 index 0000000000..e681942f83 --- /dev/null +++ b/packages/composer-client/lib/identityregistry.js @@ -0,0 +1,148 @@ +/* + * 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. + */ + +'use strict'; + +const Registry = require('./registry'); +const Util = require('composer-common').Util; + +const REGISTRY_TYPE = 'Asset'; + +/** + * The IdentityRegistry is used to store a set of identities on the blockchain. + *

+ * @extends Registry + * @see See [Registry]{@link module:composer-client.Registry} + * @class + * @memberof module:composer-client + */ +class IdentityRegistry extends Registry { + + /** + * Get an existing identity registry. + * + * @param {SecurityContext} securityContext The user's security context. + * @param {ModelManager} modelManager The ModelManager to use for this identity registry. + * @param {Factory} factory The factory to use for this identity registry. + * @param {Serializer} serializer The Serializer to use for this identity registry. + * @return {Promise} A promise that will be resolved with a {@link IdentityRegistry} + * instance representing the identity registry. + */ + static getIdentityRegistry(securityContext, modelManager, factory, serializer) { + Util.securityCheck(securityContext); + if (!modelManager) { + throw new Error('modelManager not specified'); + } else if (!factory) { + throw new Error('factory not specified'); + } else if (!serializer) { + throw new Error('serializer not specified'); + } + return Registry.getAllRegistries(securityContext, REGISTRY_TYPE) + .then((registries) => { + return registries.map((registry) => { + if (registry.id === 'org.hyperledger.composer.system.Identity') { + return new IdentityRegistry(registry.id, registry.name, securityContext, modelManager, factory, serializer); + } + })[0]; + }); + } + + /** + * Create an identity registry. + * Note: Only to be called by framework code. Applications should + * retrieve instances from {@link BusinessNetworkConnection} + *

+ * + * @param {string} id The unique identifier of the identity registry. + * @param {string} name The display name for the identity registry. + * @param {SecurityContext} securityContext The security context to use for this asset registry. + * @param {ModelManager} modelManager The ModelManager to use for this identity registry. + * @param {Factory} factory The factory to use for this identity registry. + * @param {Serializer} serializer The Serializer to use for this identity registry. + * @private + */ + constructor(id, name, securityContext, modelManager, factory, serializer) { + super(REGISTRY_TYPE, id, name, securityContext, modelManager, factory, serializer); + } + + /** + * Unsupported operation; you cannot add an identity to an identity + * registry. + * + * @param {Resource} resource The resource to be added to the registry. + * @param {string} data The data for the resource. + * @private + */ + add(resource) { + throw new Error('cannot add identity to an identity registry'); + } + + /** + * Unsupported operation; you cannot add an identity to an identity + * registry. + * + * @param {Resource[]} resources The resources to be added to the registry. + * @private + */ + addAll(resources) { + throw new Error('cannot add identities to a identity registry'); + } + + /** + * Unsupported operation; you cannot update an identity in an identity + * registry. This method will always throw an exception when called. + * + * @param {Resource} resource The resource to be updated in the registry. + * @private + */ + update(resource) { + throw new Error('cannot update identities in an identity registry'); + } + + /** + * Unsupported operation; you cannot update an identity in an identity + * registry. + * + * @param {Resource[]} resources The resources to be updated in the asset registry. + * @private + */ + updateAll(resources) { + throw new Error('cannot update identities in an identity registry'); + } + + /** + * Unsupported operation; you cannot remove an identity from an identity + * registry. This method will always throw an exception when called. + * + * @param {(Resource|string)} resource The resource, or the unique identifier of the resource. + * @private + */ + remove(resource) { + throw new Error('cannot remove identities from an identity registry'); + } + + /** + * Unsupported operation; you cannot remove an identity from an identity + * registry. This method will always throw an exception when called. + * + * @param {(Resource[]|string[])} resources The resources, or the unique identifiers of the resources. + * @private + */ + removeAll(resources) { + throw new Error('cannot remove identities from an identity registry'); + } + +} + +module.exports = IdentityRegistry; diff --git a/packages/composer-client/test/businessnetworkconnection.js b/packages/composer-client/test/businessnetworkconnection.js index 660c660d08..f58c48e1f5 100644 --- a/packages/composer-client/test/businessnetworkconnection.js +++ b/packages/composer-client/test/businessnetworkconnection.js @@ -33,6 +33,8 @@ const Resource = require('composer-common').Resource; const SecurityContext = require('composer-common').SecurityContext; const TransactionDeclaration = require('composer-common').TransactionDeclaration; const TransactionRegistry = require('../lib/transactionregistry'); +const IdentityRegistry = require('../lib/identityregistry'); +const Registry = require('../lib/registry'); const Util = require('composer-common').Util; const uuid = require('uuid'); const version = require('../package.json').version; @@ -55,6 +57,7 @@ describe('BusinessNetworkConnection', () => { let mockQueryFile; let mockFactory; let mockSerializer; + let mockParticipantRegistry; beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -75,6 +78,7 @@ describe('BusinessNetworkConnection', () => { mockSerializer = sinon.createStubInstance(Serializer); businessNetworkConnection.businessNetwork.getSerializer.returns(mockSerializer); businessNetworkConnection.securityContext = mockSecurityContext; + mockParticipantRegistry = sinon.createStubInstance(ParticipantRegistry); delete process.env.COMPOSER_CONFIG; }); @@ -946,6 +950,11 @@ describe('BusinessNetworkConnection', () => { userID: 'dogeid1', userSecret: 'suchsecret' }); + + mockParticipantRegistry.exists.resolves(true); + businessNetworkConnection.getParticipantRegistry = () => { + return Promise.resolve(mockParticipantRegistry); + }; }); it('should throw if participant not specified', () => { @@ -1009,6 +1018,59 @@ describe('BusinessNetworkConnection', () => { }); }); + it('should throw if participant does not exist', () => { + sandbox.stub(Util, 'invokeChainCode').resolves(); + mockParticipantRegistry.exists.resolves(false); + + return businessNetworkConnection.issueIdentity('org.doge.Doge#DOGE_1', 'dogeid1') + .catch((error) => { + error.should.match(/does not exist /); + }); + }); + }); + + describe('#bindIdentity', () => { + + const pem = '-----BEGIN CERTIFICATE-----\nMIIB8jCCAZmgAwIBAgIULKt4c4xcdMwGgjNef9IL92HQkyAwCgYIKoZIzj0EAwIw\nczELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh\nbiBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT\nE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNzA4MTg1NzAwWhcNMTgwNzA4MTg1\nNzAwWjASMRAwDgYDVQQDEwdib29iaWVzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD\nQgAE5P4RNqfEy8pArDxAbVIjRxqkwlpHUY7ANR6X7a4uvVIzIPDx4p7lf37xuc+5\nI9VZCvcI1SA5nIRphet0yYSgZaNsMGowDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwEB\n/wQCMAAwHQYDVR0OBBYEFAmjJfUZvdB8pHvklsdd1HiVog+VMCsGA1UdIwQkMCKA\nIBmrZau7BIB9rRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0cA\nMEQCIGtqR9rUR2ESu2UfUpNUfEeeBsshMkMHmuP/r5uvo2fSAiBtFB9Aid/3nexB\nI5qkVbdRSRQpt7uxoKFDLV/LUDM9xw==\n-----END CERTIFICATE-----\n'; + + beforeEach(() => { + businessNetworkConnection.connection = mockConnection; + }); + + it('should throw if participant not specified', () => { + (() => { + businessNetworkConnection.bindIdentity(null, pem); + }).should.throw(/participant not specified/); + }); + + it('should throw if certificate not specified', () => { + (() => { + let mockResource = sinon.createStubInstance(Resource); + mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1'); + businessNetworkConnection.bindIdentity(mockResource, null); + }).should.throw(/certificate not specified/); + }); + + it('should submit a request to the chaincode for a resource', () => { + sandbox.stub(Util, 'invokeChainCode').resolves(); + let mockResource = sinon.createStubInstance(Resource); + mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1'); + return businessNetworkConnection.bindIdentity(mockResource, pem) + .then(() => { + sinon.assert.calledOnce(Util.invokeChainCode); + sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', pem]); + }); + }); + + it('should submit a request to the chaincode for a fully qualified identifier', () => { + sandbox.stub(Util, 'invokeChainCode').resolves(); + return businessNetworkConnection.bindIdentity('org.doge.Doge#DOGE_1', pem) + .then(() => { + sinon.assert.calledOnce(Util.invokeChainCode); + sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', pem]); + }); + }); + }); describe('#bindIdentity', () => { @@ -1076,4 +1138,37 @@ describe('BusinessNetworkConnection', () => { }); + describe('#getIdentityRegistry', () => { + + it('should perform a security check', () => { + + // Set up the mock. + let stub = sandbox. stub(Util, 'securityCheck'); + let identityRegistry = sinon.createStubInstance(IdentityRegistry); + identityRegistry.id = 'org.hyperledger.composer.system.Identity'; + identityRegistry.name = 'such registry'; + sandbox.stub(Registry, 'getAllRegistries').resolves([identityRegistry]); + + // Invoke the function. + return businessNetworkConnection + .getIdentityRegistry() + .then(() => { + sinon.assert.calledTwice(stub); + }); + + }); + + it('should throw when the default identity registry does not exist', () => { + + // Set up the mock. + sandbox.stub(IdentityRegistry, 'getIdentityRegistry').resolves(null); + + // Invoke the function. + return businessNetworkConnection + .getIdentityRegistry() + .should.be.rejectedWith(/default identity registry/); + + }); + }); + }); diff --git a/packages/composer-client/test/identityregistry.js b/packages/composer-client/test/identityregistry.js new file mode 100644 index 0000000000..8c67bab26b --- /dev/null +++ b/packages/composer-client/test/identityregistry.js @@ -0,0 +1,193 @@ +/* + * 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. + */ + +'use strict'; + +const Factory = require('composer-common').Factory; +const ModelManager = require('composer-common').ModelManager; +const IdentityRegistry = require('../lib/identityregistry'); +const Registry = require('../lib/registry'); +const SecurityContext = require('composer-common').SecurityContext; +const Serializer = require('composer-common').Serializer; +const Util = require('composer-common').Util; + +const chai = require('chai'); +chai.should(); +chai.use(require('chai-things')); +const sinon = require('sinon'); + +describe('IdentityRegistry', () => { + + let sandbox; + let mockSecurityContext; + let mockModelManager; + let mockFactory; + let mockSerializer; + let registry; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockSecurityContext = sinon.createStubInstance(SecurityContext); + mockModelManager = sinon.createStubInstance(ModelManager); + mockFactory = sinon.createStubInstance(Factory); + mockSerializer = sinon.createStubInstance(Serializer); + registry = new IdentityRegistry('org.hyperledger.composer.system.Identity', 'wowsuchregistry', mockSecurityContext, mockModelManager, mockFactory, mockSerializer); + sandbox.stub(Util, 'securityCheck'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#getIdentityRegistry', () => { + + it('should throw when modelManager not specified', () => { + (function () { + IdentityRegistry.getIdentityRegistry(mockSecurityContext, null, mockFactory, mockSerializer); + }).should.throw(/modelManager not specified/); + }); + + it('should throw when factory not specified', () => { + (function () { + IdentityRegistry.getIdentityRegistry(mockSecurityContext, mockModelManager, null, mockSerializer); + }).should.throw(/factory not specified/); + }); + + it('should throw when serializer not specified', () => { + (function () { + IdentityRegistry.getIdentityRegistry(mockSecurityContext, mockModelManager, mockFactory, null); + }).should.throw(/serializer not specified/); + }); + + it('should invoke the chain-code and return the transaction registry', () => { + + // Set up the responses from the chain-code. + sandbox.stub(Registry, 'getAllRegistries', () => { + return Promise.resolve( + [{id: 'org.hyperledger.composer.system.Identity', name: 'doge registry'}] + ); + }); + + // Invoke the getAllTransactionRegistries function. + return IdentityRegistry + .getIdentityRegistry(mockSecurityContext, mockModelManager, mockFactory, mockSerializer) + .then((identityRegistry) => { + + // Check that the registry was requested correctly. + sinon.assert.calledWith(Util.securityCheck, mockSecurityContext); + sinon.assert.calledOnce(Registry.getAllRegistries); + sinon.assert.calledWith(Registry.getAllRegistries, mockSecurityContext, 'Asset'); + + // Check that the transaction registries were returned correctly. + identityRegistry.should.be.an.instanceOf(IdentityRegistry); + identityRegistry.id.should.equal('org.hyperledger.composer.system.Identity'); + identityRegistry.name.should.equal('doge registry'); + + }); + + }); + + it('should handle an error from the chain-code', () => { + + // Set up the responses from the chain-code. + sandbox.stub(Registry, 'getAllRegistries', () => { + return Promise.reject( + new Error('failed to invoke chain-code') + ); + }); + + // Invoke the getAllTransactionRegistries function. + return IdentityRegistry + .getIdentityRegistry(mockSecurityContext, mockModelManager, mockFactory, mockSerializer) + .then((identityRegistry) => { + throw new Error('should not get here'); + }).catch((error) => { + error.should.match(/failed to invoke chain-code/); + }); + + }); + + it('should not throw if Identity registry is not found', () => { + sandbox.stub(Registry, 'getAllRegistries', () => { + return Promise.resolve( + [{id: 'org.hyperledger.composer.system.Doge', name: 'doge registry'}] + ); + }); + (function () { + IdentityRegistry.getIdentityRegistry(mockSecurityContext, mockModelManager, mockFactory, mockSerializer); + }).should.not.throw(); + }); + }); + + describe('#add', () => { + + it('should throw an unsupported operation when called', () => { + (() => { + registry.add(null); + }).should.throw(/cannot add identity to an identity registry/); + }); + + }); + + describe('#addAll', () => { + + it('should throw an unsupported operation when called', () => { + (() => { + registry.addAll(null); + }).should.throw(/cannot add identities to a identity registry/); + }); + + }); + + describe('#update', () => { + + it('should throw an unsupported operation when called', () => { + (() => { + registry.update(null); + }).should.throw(/cannot update identities in an identity registry/); + }); + + }); + + describe('#updateAll', () => { + + it('should throw an unsupported operation when called', () => { + (() => { + registry.updateAll(null); + }).should.throw(/cannot update identities in an identity registry/); + }); + + }); + + describe('#remove', () => { + + it('should throw an unsupported operation when called', () => { + (() => { + registry.remove('dogecar1'); + }).should.throw(/cannot remove identities from an identity registry/); + }); + + }); + + describe('#removeAll', () => { + + it('should throw an unsupported operation when called', () => { + (() => { + registry.removeAll(null); + }).should.throw(/cannot remove identities from an identity registry/); + }); + + }); + +}); diff --git a/packages/composer-common/lib/introspect/classdeclaration.js b/packages/composer-common/lib/introspect/classdeclaration.js index fa80673402..da89dbf9a1 100644 --- a/packages/composer-common/lib/introspect/classdeclaration.js +++ b/packages/composer-common/lib/introspect/classdeclaration.js @@ -167,6 +167,17 @@ class ClassDeclaration { if(classDecl===null) { throw new IllegalModelException('Could not find super type ' + this.superType, this.modelFile, this.ast.location); } + + // Prevent extending declaration with different type of declaration + const supertypeDeclaration = this.getModelFile().getType(this.superType); + if (supertypeDeclaration) { + if (this.constructor.name !== supertypeDeclaration.constructor.name) { + let typeName = this.getSystemType(); + let superTypeName = supertypeDeclaration.getSystemType(); + throw new IllegalModelException(`${typeName} (${this.getName()}) cannot extend ${superTypeName} (${supertypeDeclaration.getName()})`, this.modelFile, this.ast.location); + } + } + // TODO (DCS) // else { // // check that assets only inherit from assets etc. diff --git a/packages/composer-common/lib/introspect/eventdeclaration.js b/packages/composer-common/lib/introspect/eventdeclaration.js index e1162227bb..edef12cf0b 100644 --- a/packages/composer-common/lib/introspect/eventdeclaration.js +++ b/packages/composer-common/lib/introspect/eventdeclaration.js @@ -16,6 +16,7 @@ const ClassDeclaration = require('./classdeclaration'); const IllegalModelException = require('./illegalmodelexception'); +const ModelUtil = require('../modelutil'); /** Class representing the definition of an Event. * @extends ClassDeclaration @@ -58,6 +59,19 @@ class EventDeclaration extends ClassDeclaration { if(!this.isSystemType() && this.getName() === 'Event') { throw new IllegalModelException('Event is a reserved type name.', this.modelFile, this.ast.location); } + + let systemTypeDeclared = true; + + // If using models without importing system models + try { + this.getModelFile().getType(ModelUtil.getSystemNamespace() + '.' + this.getSystemType()); + } catch (e) { + systemTypeDeclared = false; + } + + if (!this.isSystemType() && this.idField && systemTypeDeclared) { + throw new IllegalModelException('Event should not specify an identifying field.', this.modelFile, this.ast.location); + } } /** diff --git a/packages/composer-common/lib/introspect/transactiondeclaration.js b/packages/composer-common/lib/introspect/transactiondeclaration.js index 4c807dfcb0..06485b78c3 100644 --- a/packages/composer-common/lib/introspect/transactiondeclaration.js +++ b/packages/composer-common/lib/introspect/transactiondeclaration.js @@ -16,6 +16,8 @@ const ClassDeclaration = require('./classdeclaration'); const IllegalModelException = require('./illegalmodelexception'); +const ModelUtil = require('../modelutil'); + /** Class representing the definition of an Transaction. * @extends ClassDeclaration @@ -58,6 +60,19 @@ class TransactionDeclaration extends ClassDeclaration { if(!this.isSystemType() && this.getName() === 'Transaction') { throw new IllegalModelException('Transaction is a reserved type name.', this.modelFile, this.ast.location); } + + let systemTypeDeclared = true; + + // If using models without importing system models + try { + this.getModelFile().getType(ModelUtil.getSystemNamespace() + '.' + this.getSystemType()); + } catch (e) { + systemTypeDeclared = false; + } + + if (!this.isSystemType() && this.idField && systemTypeDeclared) { + throw new IllegalModelException('Transaction should not specify an identifying field.', this.modelFile, this.ast.location); + } } } diff --git a/packages/composer-common/lib/system.cto b/packages/composer-common/lib/system.cto index 84618d4c98..ec816a76fb 100644 --- a/packages/composer-common/lib/system.cto +++ b/packages/composer-common/lib/system.cto @@ -4,7 +4,7 @@ abstract asset Asset { } abstract participant Participant { } -abstract transaction Transaction identified by transactionId{ +abstract transaction Transaction { o String transactionId o DateTime timestamp } diff --git a/packages/composer-common/test/data/model/concept2.cto b/packages/composer-common/test/data/model/concept2.cto index a6b1ec85f8..8a568c395d 100755 --- a/packages/composer-common/test/data/model/concept2.cto +++ b/packages/composer-common/test/data/model/concept2.cto @@ -146,18 +146,18 @@ asset POContractorRecord identified by poContractorKey { *****************************/ /** Create purchase order from PO provider, ie SAP system */ -transaction CreatePO identified by trxId { +transaction CreatePO { o String trxId o PurchaseOrder po } -transaction UpdatePOContractorSupplierDetails identified by trxId { +transaction UpdatePOContractorSupplierDetails { o String trxId o String poNumber o POContractorSupplierDetails poContractorSupplierDetails } -transaction UpdatePOContractorBuyerDetails identified by trxId { +transaction UpdatePOContractorBuyerDetails { o String trxId o String poNumber o POContractorBuyerDetails poContractorBuyerDetails @@ -166,7 +166,7 @@ transaction UpdatePOContractorBuyerDetails identified by trxId { /** * Transaction for supplier account manager to approve purchase order buyer details */ -transaction ApprovePOContractorBuyerDetails identified by trxId { +transaction ApprovePOContractorBuyerDetails { o String trxId o String poNumber } @@ -174,7 +174,7 @@ transaction ApprovePOContractorBuyerDetails identified by trxId { /** * Transaction for buyer manager to approve purchase order supplier details */ -transaction ApprovePOContractorSupplierDetails identified by trxId { +transaction ApprovePOContractorSupplierDetails { o String trxId o String poNumber } diff --git a/packages/composer-common/test/data/model/dependencies/contract/proforma.cto b/packages/composer-common/test/data/model/dependencies/contract/proforma.cto index 183aedf18b..0bff5acfa3 100644 --- a/packages/composer-common/test/data/model/dependencies/contract/proforma.cto +++ b/packages/composer-common/test/data/model/dependencies/contract/proforma.cto @@ -41,13 +41,13 @@ asset ProForma identified by id { o Clause[] clauses } -transaction CreateProForma identified by id { +transaction CreateProForma { o String id o String description o Clause[] clauses } -transaction UpdateProForma identified by id { +transaction UpdateProForma { o String id o String description o Clause[] clauses diff --git a/packages/composer-common/test/data/model/farm2fork.cto b/packages/composer-common/test/data/model/farm2fork.cto index 8aa35247d2..d0ceba4d77 100644 --- a/packages/composer-common/test/data/model/farm2fork.cto +++ b/packages/composer-common/test/data/model/farm2fork.cto @@ -57,7 +57,7 @@ abstract transaction AnimalTransaction { } -transaction Foo extends Meat { +transaction Foo { } diff --git a/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto b/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto index 901e107513..82c4d9e821 100644 --- a/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto +++ b/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto @@ -1,11 +1,11 @@ namespace com.testing -transaction AssetTransaction identified by id { +transaction AssetTransaction { o String id o String newValue } -transaction AssetTransaction identified by id { +transaction AssetTransaction { o String id o String newValue } \ No newline at end of file diff --git a/packages/composer-common/test/introspect/functiondeclaration.js b/packages/composer-common/test/introspect/functiondeclaration.js index 52f6cbd9f3..781bf3a4c6 100644 --- a/packages/composer-common/test/introspect/functiondeclaration.js +++ b/packages/composer-common/test/introspect/functiondeclaration.js @@ -25,7 +25,7 @@ const sinon = require('sinon'); describe('FunctionDeclaration', () => { const modelManager = new ModelManager(); - modelManager.addModelFile('namespace org.acme transaction TestTransaction identified by id {o String id}'); + modelManager.addModelFile('namespace org.acme transaction TestTransaction {}'); let mozartModel = fs.readFileSync('test/data/model/mozart.cto', 'utf8'); modelManager.addModelFile(mozartModel, 'mozart.cto'); diff --git a/packages/composer-common/test/introspect/transactiondeclaration.js b/packages/composer-common/test/introspect/transactiondeclaration.js index e384e121f5..952d897260 100644 --- a/packages/composer-common/test/introspect/transactiondeclaration.js +++ b/packages/composer-common/test/introspect/transactiondeclaration.js @@ -23,20 +23,17 @@ require('chai').should(); const sinon = require('sinon'); describe('TransactionDeclaration', () => { - - let mockModelManager; let mockSystemTransaction; let mockClassDeclaration; let sandbox; + let modelManager; beforeEach(() => { sandbox = sinon.sandbox.create(); - mockModelManager = sinon.createStubInstance(ModelManager); + modelManager = new ModelManager(); mockSystemTransaction = sinon.createStubInstance(TransactionDeclaration); mockSystemTransaction.getFullyQualifiedName.returns('org.hyperledger.composer.system.Transaction'); - mockModelManager.getSystemTypes.returns([mockSystemTransaction]); mockClassDeclaration = sinon.createStubInstance(ClassDeclaration); - mockModelManager.getType.returns(mockClassDeclaration); mockClassDeclaration.getProperties.returns([]); }); @@ -45,15 +42,15 @@ describe('TransactionDeclaration', () => { }); describe('#validate', () => { + it('should throw error name is Transaction', () => { const model = ` namespace com.test - transaction Transaction identified by transactionId { - o String transactionId + transaction Transaction { }`; - const modelFile = new ModelFile(mockModelManager, model); + const modelFile = new ModelFile(modelManager, model); const p = modelFile.getTransactionDeclarations()[0]; (() => { diff --git a/packages/composer-playground/e2e/data/bna/basic-sample-network.bna b/packages/composer-playground/e2e/data/bna/basic-sample-network.bna index 5bce114bb238f88a395565cae82bfa4162e565d0..2404572cb450a010dac2c295bc6d44aeb87fc380 100644 GIT binary patch literal 6444 zcmb_gPmdc(75B3LgaN@yNN{{LxB+cVkCqMaoHCz%kF>`XM{Y_tk2Q|>O0JJas& zt*Rbp6b%P1NL)B@mOzL3oRS@NiBG*y{d%>4G z;V0ovaM=$sv5;7p3Y|m)Efz&82bna>T%85|;9M%5t3UwF5 zc@z9$9l$kI>5yeW{+YZ0%3?s_dEoE?$u*Pra|OrW^aD|vdCn$nLK%3fIDlY;G?>f5 z%?h;V_hlioSY}b;l3}XyC30bgW-di6RcKx^nx|gqqK~X70&_Nm3k03L!l~_)e28eA^hOm#ccDU*m4(QyjdssabOGwM}@&`9<&;=jAI13Q$+Kw3Y}$}HkCT|c9U zReP70Pg))1Uw{1l&t~s-I{f`MI>^!f=>BK>;UfMCJ(DR?Qj*Cx7WQl1?qr^5!asvn z5O}>HcvCY7D_)oT6w4VaNP?!N=3knsK2)~?+g=1=^E8ztw3ddrEK%>|*fy>aFP&`n zA)_KHghFvm3e?y(>62!o#bDq=Ev(<#43lQVGGuXciabXY+T{DZbg~T_eckj4x|yVW znaP-@7m!fx`szS^2xoVmi?o!4;)9NAE-23$24^ZoBBQA!WI-}ZoJ?I#7l|=&QbX-4 z9}+k1>Rf+=B>33>)t)l}iU>nlZukJ~76sB)SX^;{SQ|U z8%{X%Ih`VY05S3)rXKS_wS3O2ZFx%0Fc)!7teoY`b;4Ndxme9DF>L;#G6WJc z9LsTPB>17|(?W1XTGP!zeuW254Ism%0|(3c%MMF1f}qj_m5xR;lpE_j(;yiNYTyUj zKqy>?7H7HjJTDPcEj`Cv1tUAg9lH`!1F^<1v{|{R!qbYD;fB!LG9U>>7gG@9rYvIM z3&uC{m+Y{WAm3A5c^cjEuv9EH1EA={RTlIR2HgET+cJCr!_I=b##Mr`SQ$VSI1uqU zg9x_Uih|0Py{7aLS1G6D*a{dP$s!ezMEx=*&v4{J4nN8;r3gdD)SK{qtzC=~vc=p-MS&>&=!p9rt_b3=)j$ab`4i!yx)C43UzuiF$dF-RN*%@31R*XjuG4L=H zUC6L~)*h97dZdMNaRGEGM|M3a0nmx;3d;Tu^#g9tCoNIu-H zY|4fA+mUkao)Bi)!U}qw{KIi zzPfEsTf6c&-?g=3MNxMY!h8Syz{$%zt!pR_k)J00yxI?c2-^)VC-yTEpXY% zmu`&<3VLTdS-M?UP@OBS3kZGDndwFbuJhyAVmhZ|qd1!_(>R8;gTbAuHh2kV2M&eZ z6@xA2oIYOaZs2Pl?a{6)(Ax43nk7mbzM1=rsi@YVfR|>K0v`0Om5oWxAJ$l39OjPC zc~W9~q#eba`)tGc-$Y=9aUwh)d1BjWSm6af-oZCbrfYZ^74QA!9~h?j`@M>y0?P~B zRk(mZc5e(~Dsd}yjoDpU>zzJ*?q0qSZdBCcW*AgV!S<&tg8SD(T<~y)X2h);{co0_ z@Z)A83&TB-@6&0*Esd7ArrVxcNnt1wk0dN3pBe#j0fqukZm_EeNf?kaHQ)Gg;a@J7 zp|JPs5cnOY&J;a-JifPoaJ6-y)liBl!9htdlT zxPJrEebV{NEHN$fx%^H@CFrXlYy4HK2djMo9+NY4vtt?`2XuFIJU;HTkZ0q`!^5W& zdNw*b8XZi=`^R*6ME4F4?vE$q!vj1$pwYn>^k{r=zfTh0LR{oayefhPP!nSUMk&lI zkacV)BU#7F&Mh1vUC0Q7as<<|(-QYpI^|oN?fqUO=ZTcXqzPuAYM8!amn#+Bc4_g` zrJ8%7T7A{Cy8QylC>Hc;t+a$LDZK2uB=gc<1{^fWmt9ZOPNKI1wTIpMylP7FWcJL< zZKNF1lV~_ZMq>hE$M7A&O^_9~Y!*X0KIcO5zD+>74W&Z2Y3V;*$$H9jp_69!8$hov z9!_3cfRk7!b0hATb_}d1SS3#xV;8GPdivpAVW~2{u8g?&9#y2ht<4^`L|d-)uz4Zu z&w4AD^{nJt$jVtv$saiFilImg_Mv4&E;&t|t&WMXYISVM4(%i@5fbN6R1NEI=DB>m zew$R}p{fl_NPqbgluXC=y zfN41I;-&uTb<8^3`cHN9ks7`yKpFDaO&SGQU@B9ga+8=(zPyYpt C=LI$Z literal 5574 zcmb7|3pCUJ|HsFMk^7~PTe+mLCQ{@!a%W*KQ)x5CT!!3ArE*&rG532Y_sFF$$}M-f z6D1<|$|biF67}2i{c8EA&VM_*owGgmdcW?^kD(qA!~h`QM$W$1{qg4CCoTXh0AuBD zZ!P6+g?DwrN;zXaF1WZk9yUcX0w!!w7Imb@XDUvBAql|=0F&<95du3QASV$hgole8 zmdqk^{$tI?6vFGEoErH(;PgZ6JfY~eCX-unq9#+-)zm5g{2>!?0J|r;wf5P9kG}o_ zAIfm7w%UYYUVAya`@*bQ*q9=~wy2H8ms=I|K81Mr)^RiNq!dqMo)P?jhWw~oix_&| zl7;47Y$E5~uNOqdM;`h{Jr)nM7JC_G{`JF4=W&6U*fGs_5;~0MgLS(vyh-f;xhiuV-NZlv6o(l)u`&`#nTlLCpW`1G~ob==RITn`Df43$3a z40=qwFW!=2op`_I(lx?;@haA@SF|T1^X@WLu}l_3-I@x3PrdFhfTXA^+_zS^hbZ%U zr`}rd1v=PYpf2sCq_38#vetV0qsydx!}7Y|wc+m)w?E#Hw(?2}Ug&4dMR&eF+1Q{3 zc(aG<+%5}8;@C#-XEM+N1>(e7Rsaq4=)edSy$ww&006`d_z|JaATMkSGCe6mD3k$A z{S*SZ6;v z4&??OVBJeqxR=#`l}oh^V7{;VI`B9l00fKUf?7_Jy+H?F;(1jeOG@;Mqz5^FH;;)! zkN;8vJUivDlRak3TOJ~(x4gA^!tI#-f5L1Q7sSDxTnc2?e>_hUBbV1u)~oN$6EE&F zk&Ih@^1%3?Xy@mLLcvATsX8Gqj4^g4>Z+#SzjnZ>o6%RPzCQgrSKj#^bWHe(uU%3~ zSFD<-u?c#Jtu|bAsYlax!B5xy;7G!EF2!BkkMAum^&0bGX=qOPh0-1huCF=z;bO@3 z8z4R4S=IePN8am;fR7%V&x5y)oe6myt?tYud^|NE+^bxfHFjT?M%7-#RTjUpJfC;< zYsOgEP_@SbpHkbN#r0kE&Kz?4wI0GolW$8ncwGjWg{K7Ib!bNcO^r{g0XQ&&rl0IL+dUw8_thDcM5LkZ#nbwp^$Se&SLaBo#~!qEysj_DJm z)+g?0iHk^xK&ofa*{(VI+J?RBA-jn(s%}BkBlk>7fZ@##BVrW>F!jCxcb!D0*g2k# z(-(_9x{U?L4W_ccV2jLR$eRszV2?c=pSWibIvxU9dlcyRT>Qy-c;lTGgi%9)?JWae zrnW7lYtbV@$%nxp9b|)3yZprA-q1u@9kXm)nMwJT z`y4~fM~I(w;I^JKr&E_CAyI4oMP}pI2lDyCUVV|SREk+!uX^ej{!TV?*>qGOP~O|5 zDUxtE>1=1NbH5(G>q2ko0bUlJB!gLlw%IR%J`p_nymXdVFe+}9Z=2#ZFR8mR^(3Uv z%^yV`WB<$4>9P&oMSXAvanyDn6OJ z*mkio=f109HFNn>(c?)`fhMpf0&7(bqOP_P_8_dp&5z%ncocRslVK+0kb&`g!zd@Y zHEo&8?Bj&yx<$I~Z>@DNsX%=eHu6fGuOAcXV^Gda(!TyGAG<%#nC!9Mz^UPE^ zdn=49(;;&|YSbCwWqR9dcH22IqS-VGziqVhp_BK$(ny~Y%yx1D3atH|(eEUYq3ywLqD&CSzkNmi37Y4#3goGJJ8C*GNSXEtQmIcgeZp z)`aDZN}S7K^PDpv@WNoTG-uZaV1haF(L{`=F6%%<`34y7+jm_Ul?l==eU^G{v12Nj zxv}ry6y2S)$7*F^U7Fg0P5nG^5eM29VRP4A=_CQ!jxMR=uEZ7AC3*3y7^{Hrcmr5G zLbSUB+D4tvJOUhY)HFCNds2s^D^z8M;fexld`j;Pov(zqSNd!{lyiHi?%arHseVkC z*?hd5z7+vnykMzxX&o~y47T;t*P>HYAkw&nKa4!nld7&;6x}!>@6bx8Ij#EIUu)W6 z+I+(3!pO9i%8B#kt#I4U=EQoVB0mM)g(kwunknRAf6uS7xz}ekCp9x58kq}<0njzi zo)qVs{24h3qxjW!#cCFg1=)t`)iZsmlA#PNroQM2Aa*!}&gkR9BAwSs4_u!(j?0GH?_h zgohL9rP{3;rZ33lO?;OM)$#1cod@%sK>Y^nLFfUs+oIuM@Tn4;{yqD zm+6{Z+@oST+-krZenGKte7!dKhltNG;8(I{#II_Eh<0a(3@^Q^z*Gt3WL4XTrM+&8Pzkk02{kA*xhbK z5oOHmU%c;(e__KPu6^Ke{F-tjd~jFf5l{NK)6@M<-@+^H-rsM%T8tPBhbnV@b+qwM zAp%CIM&;srM*8Kh-4g3Se;clUf7yRYd|f@?lRr7Dcy>3}%OPPD_9^qt8ktXF)cY|V zOYY|DGqkk}s3rHcI1VQEeOKn;qH6FcB?a@n6Pe6RImK!%yJGdlMxJT~>6x$m2!)iX zX7xc*cT)pWhT@~~ubM*6z)Vy}nqeY=d0%K2?y{bz8OFQJI`Z2ae1{o+EWvWHYMe!7xG#h5mA7ByP7spe}0BazCL<& zb#+KarB1?|;{j> z|KZMzbVd1^B7YmhajPa=Ujy`TpFUl>P+#`VkR^fdnWXPE-FZCtaW|B$9uaEgWd}p1 zAZkoIgx6rU+D0cQWWec;Fn%sZ+CDaq=VgtiZHgZho=LW43_PANFJPMt1PW!hB8>Q9 zH-*%1<}ytb%~#4~(OAqH#y=lWlkdtl?&zuq1!UZfZNHfFnQeItWgB-k#ImWQ+wEM( zd2xT{81gIVPYD;?Qe}#Bwi+wadQO$zwZg zq<(PbSRoEggXonKu%EEQzN;D+^5fP_j#j!9z|1l|l6WR~J!*g{*!2q^FV z2}hw|!CO~6Ho2Q-{$FMO)65OnTT*M>Y96M@P1DHQ`cDW-(>7pyB;RZy{&?h{+@D9L z6wC*IIWq3A++Xzj?@zzIW$;k^#Via+8VbL)cvFXOdV{Q+DFa1DQi_)Slt^FQKOreJ zIe1HxQ+j&4xqOst*iq6t@mq7ZM9|I_Q*QcVlvLkqf1y$eqU~1yye>hx^6dMC|A=k} ztAAAwJEJK_xX3SP`dvGrw{K9tr;|r1<&YKng&s*Nl;67hmuU*#S_&wWgiNL!ViFYO zG+#rezm?3kk^TEWA!iF^ccQGzSzle|9n7bgj+AbpA>{!8z)ZTo1_1!QtmIGs1BtR% A*8l(j diff --git a/packages/composer-playground/e2e/tests/editor-define.spec.ts b/packages/composer-playground/e2e/tests/editor-define.spec.ts index 94c89d219c..3687e6a275 100644 --- a/packages/composer-playground/e2e/tests/editor-define.spec.ts +++ b/packages/composer-playground/e2e/tests/editor-define.spec.ts @@ -1,4 +1,4 @@ -import { browser, element, by } from 'protractor'; +import { browser, element, by } from 'protractor'; import { ExpectedConditions } from 'protractor'; import { OperationsHelper } from '../utils/operations-helper.ts'; import { EditorHelper } from '../utils/editor-helper.ts'; @@ -16,12 +16,14 @@ let should = chai.should(); describe('Editor Define', (() => { - // Navigate to Editor base page and move past welcome splash - beforeAll(() => { - OperationsHelper.navigatePastWelcome(); - }); - describe('On initialise', (() => { + + // Navigate to Editor base page and move past welcome splash + beforeAll(() => { + browser.get(browser.baseUrl); + OperationsHelper.navigatePastWelcome(); + }); + it('should initialise the side navigation with basic sample network', (() => { // Check Files (Basic Sample Files) Present and clickable // -within