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/); }); });