From 54c6ea2ea47e626188f21883a8c396634d6640e0 Mon Sep 17 00:00:00 2001
From: Simon Stone
Date: Wed, 12 Jul 2017 13:10:09 +0100
Subject: [PATCH 1/7] Initial drop of identity registry (#1539)
---
packages/composer-client/api.txt | 1 +
packages/composer-client/changelog.txt | 3 +
.../lib/businessnetworkconnection.js | 111 ++++-
.../test/businessnetworkconnection.js | 138 +++++-
packages/composer-common/lib/modelmanager.js | 38 ++
.../composer-runtime-hlfv1/identityservice.go | 121 +++--
.../lib/compiledscriptbundle.js | 5 +-
packages/composer-runtime/lib/context.js | 119 +++--
.../composer-runtime/lib/engine.identities.js | 71 ++-
packages/composer-runtime/lib/engine.js | 21 +-
.../composer-runtime/lib/engine.logging.js | 2 +-
packages/composer-runtime/lib/httpservice.js | 6 +-
.../composer-runtime/lib/identitymanager.js | 429 +++++++++++++----
.../composer-runtime/lib/identityservice.js | 34 +-
.../composer-runtime/lib/queryexecutor.js | 2 +-
.../composer-runtime/lib/transactionlogger.js | 6 +-
packages/composer-runtime/test/context.js | 196 +++++---
.../test/engine.identities.js | 68 ++-
packages/composer-runtime/test/engine.js | 28 +-
.../composer-runtime/test/identitymanager.js | 454 +++++++++++++-----
.../composer-runtime/test/identityservice.js | 34 +-
.../composer-systests/systest/identities.js | 4 +-
22 files changed, 1452 insertions(+), 439 deletions(-)
diff --git a/packages/composer-client/api.txt b/packages/composer-client/api.txt
index a7619c43f3..59b72cc7e8 100644
--- a/packages/composer-client/api.txt
+++ b/packages/composer-client/api.txt
@@ -24,6 +24,7 @@ class BusinessNetworkConnection extends EventEmitter {
+ Promise query(Object)
+ Promise ping()
+ Promise issueIdentity(string,object,boolean)
+ + Promise bindIdentity(string)
+ Promise revokeIdentity(string)
}
class ParticipantRegistry extends Registry {
diff --git a/packages/composer-client/changelog.txt b/packages/composer-client/changelog.txt
index 9e2dcb3813..42108c1389 100644
--- a/packages/composer-client/changelog.txt
+++ b/packages/composer-client/changelog.txt
@@ -12,6 +12,9 @@
# Note that the latest public API is documented using JSDocs and is available in api.txt.
#
+Version 0.9.2 {465d5a96640dce8240a392d058e06fe5} 2017-07-11
+- Added bindIdentity
+
Version 0.9.0 {d6ebf2b722345ee0f7dac56f42730f12} 2017-06-26
- Added buildQuery and query
- Removed deprecated BusinesNetworkConnection.existsAssetRegistry method
diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js
index 4c10c8e111..bace44d451 100644
--- a/packages/composer-client/lib/businessnetworkconnection.js
+++ b/packages/composer-client/lib/businessnetworkconnection.js
@@ -322,7 +322,7 @@ class BusinessNetworkConnection extends EventEmitter {
})
.then((securityContext) => {
this.securityContext = securityContext;
- return this.connection.ping(this.securityContext);
+ return this.ping();
})
.then(() => {
return Util.queryChainCode(this.securityContext, 'getBusinessNetwork', []);
@@ -525,16 +525,64 @@ class BusinessNetworkConnection extends EventEmitter {
* been tested. The promise will be rejected if the version is incompatible.
*/
ping() {
+ const method = 'ping';
+ LOG.entry(method);
+ return this.pingInner()
+ .catch((error) => {
+ if (error.message.match(/ACTIVATION_REQUIRED/)) {
+ LOG.debug(method, 'Activation required, activating ...');
+ return this.activate()
+ .then(() => {
+ return this.pingInner();
+ });
+ }
+ throw error;
+ })
+ .then((result) => {
+ LOG.exit(method, result);
+ return result;
+ });
+ }
+
+ /**
+ * Test the connection to the runtime and verify that the version of the
+ * runtime is compatible with this level of the client node.js module.
+ * @private
+ * @return {Promise} A promise that will be fufilled when the connection has
+ * been tested. The promise will be rejected if the version is incompatible.
+ */
+ pingInner() {
+ const method = 'pingInner';
+ LOG.entry(method);
Util.securityCheck(this.securityContext);
- return this.connection.ping(this.securityContext);
+ return this.connection.ping(this.securityContext)
+ .then((result) => {
+ LOG.exit(method, result);
+ return result;
+ });
}
/**
- * Issue an identity with the specified user ID and map it to the specified
+ * Activate the current identity on the currently connected business network.
+ * @private
+ * @return {Promise} A promise that will be fufilled when the connection has
+ * been tested. The promise will be rejected if the version is incompatible.
+ */
+ activate() {
+ const method = 'activate';
+ LOG.entry(method);
+ return Util.invokeChainCode(this.securityContext, 'activateIdentity', [])
+ .then(() => {
+ LOG.exit(method);
+ });
+ }
+
+ /**
+ * Issue an identity with the specified name and map it to the specified
* participant.
* @param {Resource|string} participant The participant, or the fully qualified
* identifier of the participant. The participant must already exist.
- * @param {string} userID The user ID for the identity.
+ * @param {string} identityName The name for the new identity.
* @param {object} [options] Options for the new identity.
* @param {boolean} [options.issuer] Whether or not the new identity should have
* permissions to create additional new identities. False by default.
@@ -543,13 +591,13 @@ class BusinessNetworkConnection extends EventEmitter {
* the participant does not exist, or if the identity is already mapped to
* another participant.
*/
- issueIdentity(participant, userID, options) {
+ issueIdentity(participant, identityName, options) {
const method = 'issueIdentity';
- LOG.entry(method, participant, userID);
+ LOG.entry(method, participant, identityName);
if (!participant) {
throw new Error('participant not specified');
- } else if (!userID) {
- throw new Error('userID not specified');
+ } else if (!identityName) {
+ throw new Error('identityName not specified');
}
let participantFQI;
if (participant instanceof Resource) {
@@ -558,9 +606,9 @@ class BusinessNetworkConnection extends EventEmitter {
participantFQI = participant;
}
Util.securityCheck(this.securityContext);
- return this.connection.createIdentity(this.securityContext, userID, options)
+ return this.connection.createIdentity(this.securityContext, identityName, options)
.then((identity) => {
- return Util.invokeChainCode(this.securityContext, 'addParticipantIdentity', [participantFQI, userID])
+ return Util.invokeChainCode(this.securityContext, 'issueIdentity', [participantFQI, identityName])
.then(() => {
LOG.exit(method, identity);
return identity;
@@ -568,24 +616,55 @@ class BusinessNetworkConnection extends EventEmitter {
});
}
+ /**
+ * Bind an existing identity to the specified participant.
+ * @param {Resource|string} participant The participant, or the fully qualified
+ * identifier of the participant. The participant must already exist.
+ * @param {string} certificate The certificate for the existing identity.
+ * @return {Promise} A promise that will be fulfilled when the identity has
+ * been added to the specified participant. The promise will be rejected if
+ * the participant does not exist, or if the identity is already mapped to
+ * another participant.
+ */
+ bindIdentity(participant, certificate) {
+ const method = 'bindIdentity';
+ LOG.entry(method, participant, certificate);
+ if (!participant) {
+ throw new Error('participant not specified');
+ } else if (!certificate) {
+ throw new Error('certificate not specified');
+ }
+ let participantFQI;
+ if (participant instanceof Resource) {
+ participantFQI = participant.getFullyQualifiedIdentifier();
+ } else {
+ participantFQI = participant;
+ }
+ Util.securityCheck(this.securityContext);
+ return Util.invokeChainCode(this.securityContext, 'bindIdentity', [participantFQI, certificate])
+ .then(() => {
+ LOG.exit(method);
+ });
+ }
+
/**
* Revoke the specified identity by removing any existing mapping to a participant.
- * @param {string} identity The identity, for example the enrollment ID.
+ * @param {string} identityId The identity, for example the enrollment ID.
* @return {Promise} A promise that will be fulfilled when the identity has
* been removed from the specified participant. The promise will be rejected if
* the participant does not exist, or if the identity is not mapped to the
* participant.
*/
- revokeIdentity(identity) {
+ revokeIdentity(identityId) {
const method = 'revokeIdentity';
- LOG.entry(method, identity);
- if (!identity) {
- throw new Error('identity not specified');
+ LOG.entry(method, identityId);
+ if (!identityId) {
+ throw new Error('identityId not specified');
}
Util.securityCheck(this.securityContext);
// It is not currently possible to revoke the certificate, so we just call
// the runtime to remove the mapping.
- return Util.invokeChainCode(this.securityContext, 'removeIdentity', [identity])
+ return Util.invokeChainCode(this.securityContext, 'revokeIdentity', [identityId])
.then(() => {
LOG.exit(method);
});
diff --git a/packages/composer-client/test/businessnetworkconnection.js b/packages/composer-client/test/businessnetworkconnection.js
index ec7d0fbb1b..660c660d08 100644
--- a/packages/composer-client/test/businessnetworkconnection.js
+++ b/packages/composer-client/test/businessnetworkconnection.js
@@ -60,6 +60,7 @@ describe('BusinessNetworkConnection', () => {
sandbox = sinon.sandbox.create();
mockSecurityContext = sinon.createStubInstance(SecurityContext);
mockConnection = sinon.createStubInstance(Connection);
+ mockSecurityContext.getConnection.returns(mockConnection);
mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition);
businessNetworkConnection = new BusinessNetworkConnection();
businessNetworkConnection.businessNetwork = mockBusinessNetworkDefinition;
@@ -858,6 +859,83 @@ describe('BusinessNetworkConnection', () => {
});
});
+ it('should throw any errors that do not match ACTIVATION_REQUIRED', () => {
+ mockConnection.ping.onFirstCall().rejects(new Error('something something ACTIVATION NOT REQUIRED'));
+ mockConnection.ping.onSecondCall().resolves(Buffer.from(JSON.stringify({
+ version: version
+ })));
+ mockConnection.invokeChainCode.withArgs(mockSecurityContext, 'activateIdentity', []).resolves();
+ businessNetworkConnection.connection = mockConnection;
+ return businessNetworkConnection.ping()
+ .should.be.rejectedWith(/ACTIVATION NOT REQUIRED/);
+ });
+
+ it('should activate the identity if the ping returns ACTIVATION_REQUIRED', () => {
+ mockConnection.ping.onFirstCall().rejects(new Error('something something ACTIVATION_REQUIRED'));
+ mockConnection.ping.onSecondCall().resolves(Buffer.from(JSON.stringify({
+ version: version
+ })));
+ mockConnection.invokeChainCode.withArgs(mockSecurityContext, 'activateIdentity', []).resolves();
+ businessNetworkConnection.connection = mockConnection;
+ return businessNetworkConnection.ping()
+ .then(() => {
+ sinon.assert.calledTwice(mockConnection.ping);
+ sinon.assert.calledOnce(mockConnection.invokeChainCode);
+ sinon.assert.calledWith(mockConnection.invokeChainCode, mockSecurityContext, 'activateIdentity', []);
+ });
+ });
+
+ });
+
+ describe('#pingInner', () => {
+
+ it('should perform a security check', () => {
+ sandbox.stub(Util, 'securityCheck');
+ mockConnection.ping.resolves(Buffer.from(JSON.stringify({
+ version: version
+ })));
+ businessNetworkConnection.connection = mockConnection;
+ return businessNetworkConnection.pingInner()
+ .then(() => {
+ sinon.assert.calledOnce(Util.securityCheck);
+ });
+ });
+
+ it('should ping the connection', () => {
+ mockConnection.ping.resolves(Buffer.from(JSON.stringify({
+ version: version
+ })));
+ businessNetworkConnection.connection = mockConnection;
+ return businessNetworkConnection.pingInner()
+ .then(() => {
+ sinon.assert.calledOnce(mockConnection.ping);
+ });
+ });
+
+ });
+
+ describe('#activate', () => {
+
+ it('should perform a security check', () => {
+ sandbox.stub(Util, 'securityCheck');
+ mockConnection.invokeChainCode.withArgs(mockSecurityContext, 'activateIdentity', []).resolves();
+ businessNetworkConnection.connection = mockConnection;
+ return businessNetworkConnection.activate()
+ .then(() => {
+ sinon.assert.calledOnce(Util.securityCheck);
+ });
+ });
+
+ it('should submit a request to the chaincode for activation', () => {
+ mockConnection.invokeChainCode.withArgs(mockSecurityContext, 'activateIdentity', []).resolves();
+ businessNetworkConnection.connection = mockConnection;
+ return businessNetworkConnection.activate()
+ .then(() => {
+ sinon.assert.calledOnce(mockConnection.invokeChainCode);
+ sinon.assert.calledWith(mockConnection.invokeChainCode, mockSecurityContext, 'activateIdentity', []);
+ });
+ });
+
});
describe('#issueIdentity', () => {
@@ -876,12 +954,12 @@ describe('BusinessNetworkConnection', () => {
}).should.throw(/participant not specified/);
});
- it('should throw if userID not specified', () => {
+ it('should throw if identityName not specified', () => {
(() => {
let mockResource = sinon.createStubInstance(Resource);
mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1');
businessNetworkConnection.issueIdentity(mockResource, null);
- }).should.throw(/userID not specified/);
+ }).should.throw(/identityName not specified/);
});
it('should submit a request to the chaincode for a resource', () => {
@@ -893,7 +971,7 @@ describe('BusinessNetworkConnection', () => {
sinon.assert.calledOnce(mockConnection.createIdentity);
sinon.assert.calledWith(mockConnection.createIdentity, mockSecurityContext, 'dogeid1');
sinon.assert.calledOnce(Util.invokeChainCode);
- sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'addParticipantIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']);
+ sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'issueIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']);
result.should.deep.equal({
userID: 'dogeid1',
userSecret: 'suchsecret'
@@ -908,7 +986,7 @@ describe('BusinessNetworkConnection', () => {
sinon.assert.calledOnce(mockConnection.createIdentity);
sinon.assert.calledWith(mockConnection.createIdentity, mockSecurityContext, 'dogeid1');
sinon.assert.calledOnce(Util.invokeChainCode);
- sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'addParticipantIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']);
+ sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'issueIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']);
result.should.deep.equal({
userID: 'dogeid1',
userSecret: 'suchsecret'
@@ -923,7 +1001,7 @@ describe('BusinessNetworkConnection', () => {
sinon.assert.calledOnce(mockConnection.createIdentity);
sinon.assert.calledWith(mockConnection.createIdentity, mockSecurityContext, 'dogeid1', { issuer: true });
sinon.assert.calledOnce(Util.invokeChainCode);
- sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'addParticipantIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']);
+ sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'issueIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1']);
result.should.deep.equal({
userID: 'dogeid1',
userSecret: 'suchsecret'
@@ -933,14 +1011,58 @@ describe('BusinessNetworkConnection', () => {
});
+ describe('#bindIdentity', () => {
+
+ const pem = '-----BEGIN CERTIFICATE-----\nMIIB8jCCAZmgAwIBAgIULKt4c4xcdMwGgjNef9IL92HQkyAwCgYIKoZIzj0EAwIw\nczELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh\nbiBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT\nE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNzA4MTg1NzAwWhcNMTgwNzA4MTg1\nNzAwWjASMRAwDgYDVQQDEwdib29iaWVzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD\nQgAE5P4RNqfEy8pArDxAbVIjRxqkwlpHUY7ANR6X7a4uvVIzIPDx4p7lf37xuc+5\nI9VZCvcI1SA5nIRphet0yYSgZaNsMGowDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwEB\n/wQCMAAwHQYDVR0OBBYEFAmjJfUZvdB8pHvklsdd1HiVog+VMCsGA1UdIwQkMCKA\nIBmrZau7BIB9rRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0cA\nMEQCIGtqR9rUR2ESu2UfUpNUfEeeBsshMkMHmuP/r5uvo2fSAiBtFB9Aid/3nexB\nI5qkVbdRSRQpt7uxoKFDLV/LUDM9xw==\n-----END CERTIFICATE-----\n';
+
+ beforeEach(() => {
+ businessNetworkConnection.connection = mockConnection;
+ });
+
+ it('should throw if participant not specified', () => {
+ (() => {
+ businessNetworkConnection.bindIdentity(null, pem);
+ }).should.throw(/participant not specified/);
+ });
+
+ it('should throw if certificate not specified', () => {
+ (() => {
+ let mockResource = sinon.createStubInstance(Resource);
+ mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1');
+ businessNetworkConnection.bindIdentity(mockResource, null);
+ }).should.throw(/certificate not specified/);
+ });
+
+ it('should submit a request to the chaincode for a resource', () => {
+ sandbox.stub(Util, 'invokeChainCode').resolves();
+ let mockResource = sinon.createStubInstance(Resource);
+ mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1');
+ return businessNetworkConnection.bindIdentity(mockResource, pem)
+ .then(() => {
+ sinon.assert.calledOnce(Util.invokeChainCode);
+ sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', pem]);
+ });
+ });
+
+ it('should submit a request to the chaincode for a fully qualified identifier', () => {
+ sandbox.stub(Util, 'invokeChainCode').resolves();
+ return businessNetworkConnection.bindIdentity('org.doge.Doge#DOGE_1', pem)
+ .then(() => {
+ sinon.assert.calledOnce(Util.invokeChainCode);
+ sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', pem]);
+ });
+ });
+
+ });
+
describe('#revokeIdentity', () => {
- it('should throw if identity not specified', () => {
+ it('should throw if identityId not specified', () => {
(() => {
let mockResource = sinon.createStubInstance(Resource);
mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1');
businessNetworkConnection.revokeIdentity(null);
- }).should.throw(/identity not specified/);
+ }).should.throw(/identityId not specified/);
});
it('should submit a request to the chaincode', () => {
@@ -948,7 +1070,7 @@ describe('BusinessNetworkConnection', () => {
return businessNetworkConnection.revokeIdentity('dogeid1')
.then(() => {
sinon.assert.calledOnce(Util.invokeChainCode);
- sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'removeIdentity', ['dogeid1']);
+ sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'revokeIdentity', ['dogeid1']);
});
});
diff --git a/packages/composer-common/lib/modelmanager.js b/packages/composer-common/lib/modelmanager.js
index e8bfcf31be..d975bd9d89 100644
--- a/packages/composer-common/lib/modelmanager.js
+++ b/packages/composer-common/lib/modelmanager.js
@@ -39,6 +39,44 @@ const SYSTEM_MODEL_CONTENTS = `
o String eventId
o DateTime timestamp
}
+
+ enum IdentityState {
+ o ISSUED
+ o BOUND
+ o ACTIVATED
+ o REVOKED
+ }
+
+ asset Identity identified by identifier extends Asset {
+ o String identifier
+ o String name
+ o String issuer
+ o String certificate
+ o IdentityState state
+ --> Participant participant
+ }
+
+ abstract transaction IdentityTransaction extends Transaction {
+
+ }
+
+ transaction IssueIdentity extends IdentityTransaction {
+ o String name
+ --> Participant participant
+ }
+
+ transaction BindIdentity extends IdentityTransaction {
+ o String identifier
+ --> Participant participant
+ }
+
+ transaction ActivateIdentity extends IdentityTransaction {
+
+ }
+
+ transaction RevokeIdentity extends IdentityTransaction {
+ o String identifier
+ }
`;
/**
diff --git a/packages/composer-runtime-hlfv1/identityservice.go b/packages/composer-runtime-hlfv1/identityservice.go
index 59a8af20cc..4d6c34fbde 100644
--- a/packages/composer-runtime-hlfv1/identityservice.go
+++ b/packages/composer-runtime-hlfv1/identityservice.go
@@ -16,8 +16,9 @@ package main
import (
"bytes"
- "strings"
+ "crypto/sha256"
"crypto/x509"
+ "encoding/hex"
"encoding/pem"
"errors"
@@ -28,8 +29,14 @@ import (
// IdentityService is a Go wrapper around an instance of the IdentityService JavaScript class.
type IdentityService struct {
- VM *duktape.Context
- Stub shim.ChaincodeStubInterface
+ VM *duktape.Context
+ Stub shim.ChaincodeStubInterface
+ Certificate *x509.Certificate
+ CurrentUserID string
+ Identifier string
+ Name string
+ Issuer string
+ PEM string
}
// NewIdentityService creates a Go wrapper around a new instance of the IdentityService JavaScript class.
@@ -43,11 +50,17 @@ func NewIdentityService(vm *duktape.Context, context *Context, stub shim.Chainco
// Create the new identity service.
result = &IdentityService{VM: vm, Stub: stub}
+ // Load the certificate.
+ err := result.loadCertificate()
+ if err != nil {
+ panic(err)
+ }
+
// Create a new instance of the JavaScript IdentityService class.
vm.PushGlobalObject() // [ global ]
vm.GetPropString(-1, "composer") // [ global composer ]
vm.GetPropString(-1, "IdentityService") // [ global composer IdentityService ]
- err := vm.Pnew(0) // [ global composer theIdentityService ]
+ err = vm.Pnew(0) // [ global composer theIdentityService ]
if err != nil {
panic(err)
}
@@ -59,65 +72,101 @@ func NewIdentityService(vm *duktape.Context, context *Context, stub shim.Chainco
vm.Pop() // [ global composer theIdentityService ]
// Bind the methods into the JavaScript object.
- vm.PushGoFunction(result.getCurrentUserID) // [ global composer theIdentityService getCurrentUserID ]
- vm.PutPropString(-2, "getCurrentUserID") // [ global composer theIdentityService ]
+ vm.PushGoFunction(result.getIdentifier) // [ global composer theIdentityService getIdentifier ]
+ vm.PutPropString(-2, "getIdentifier") // [ global composer theIdentityService ]
+ vm.PushGoFunction(result.getName) // [ global composer theIdentityService getName ]
+ vm.PutPropString(-2, "getName") // [ global composer theIdentityService ]
+ vm.PushGoFunction(result.getIssuer) // [ global composer theIdentityService getIssuer ]
+ vm.PutPropString(-2, "getIssuer") // [ global composer theIdentityService ]
+ vm.PushGoFunction(result.getCertificate) // [ global composer theIdentityService getCertificate ]
+ vm.PutPropString(-2, "getCertificate") // [ global composer theIdentityService ]
// Return the new identity service.
return result
}
-// extract the common name from the creator x509 certificate
+// load the creator x509 certificate
// Although we should really use protobufs to correctly locate the certificate
// this would have meant pulling in a load of vendor dependencies as they aren't
// in the chaincode container and it isn't worth it.
-func extractNameFromCreator(stub shim.ChaincodeStubInterface) (result string, errResp error) {
- logger.Debug("Entering extractNameFromCreator", &stub)
- defer func() {logger.Debug("Exiting extractNameFromCreator", result, errResp)}()
- creator, err := stub.GetCreator()
+func (identityService *IdentityService) loadCertificate() (errResp error) {
+ logger.Debug("Entering loadCertificate")
+ defer func() { logger.Debug("Exiting loadCertificate", errResp) }()
+
+ creator, err := identityService.Stub.GetCreator()
if err != nil {
- return "", err
+ return err
}
logger.Debug("creator", string(creator))
- certStart := bytes.Index(creator,[]byte("-----BEGIN CERTIFICATE-----"))
+ certStart := bytes.Index(creator, []byte("-----BEGIN CERTIFICATE-----"))
if certStart == -1 {
- return "", errors.New("No Certificate found")
+ return errors.New("No Certificate found")
}
certText := creator[certStart:]
block, _ := pem.Decode(certText)
if block == nil {
- return "", errors.New("Error received on pem.Decode of certificate:" + string(certText))
+ return errors.New("Error received on pem.Decode of certificate:" + string(certText))
}
ucert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
- return "", err
+ return err
}
+ identityService.Certificate = ucert
+ identityService.PEM = string(certText)
+ return nil
+}
+
+// getIdentifier gets a unique identifier for the identity used to submit the transaction.
+func (identityService *IdentityService) getIdentifier(vm *duktape.Context) (result int) {
+ logger.Debug("Entering IdentityService.getIdentifier", vm)
+ defer func() { logger.Debug("Exiting IdentityService.getIdentifier", result) }()
+
+ // Create a fingerprint of the certificate.
+ bytes := identityService.Certificate.Raw
+ hash := sha256.New()
+ hash.Write(bytes)
+ fingerprint := hex.EncodeToString(hash.Sum(nil))
- return ucert.Subject.CommonName, nil
+ // Return the fingerprint.
+ vm.PushString(fingerprint)
+ return 1
}
-// getCurrentUserID retrieves the userID attribute from the users certificate.
-func (identityService *IdentityService) getCurrentUserID(vm *duktape.Context) (result int) {
- logger.Debug("Entering IdentityService.getCurrentUserID", vm)
- defer func() { logger.Debug("Exiting IdentityService.getCurrentUserID", result) }()
+// getName gets the name of the identity used to submit the transaction.
+func (identityService *IdentityService) getName(vm *duktape.Context) (result int) {
+ logger.Debug("Entering IdentityService.getName", vm)
+ defer func() { logger.Debug("Exiting IdentityService.getName", result) }()
- creatorName, err := extractNameFromCreator(identityService.Stub)
- if err != nil {
- vm.PushErrorObjectVa(duktape.ErrError, "%s", err.Error())
- vm.Throw()
- return 0
- }
+ // Return the common name of the certificate.
+ name := identityService.Certificate.Subject.CommonName
+ vm.PushString(name)
+ return 1
+}
- logger.Debug("Common Name", creatorName)
+// getIssuer gets the issuer of the identity used to submit the transaction.
+func (identityService *IdentityService) getIssuer(vm *duktape.Context) (result int) {
+ logger.Debug("Entering IdentityService.getIssuer", vm)
+ defer func() { logger.Debug("Exiting IdentityService.getIssuer", result) }()
- // TODO: Will be upgraded to a new security model soon.
- // returning Null grants any common name with the word admin in
- // it to have all authority
- if strings.Contains(strings.ToLower(creatorName), "admin") {
- vm.PushNull()
- return 1
- }
- vm.PushString(creatorName)
+ // Create a fingerprint of the issuer of the certificate.
+ bytes := identityService.Certificate.RawIssuer
+ hash := sha256.New()
+ hash.Write(bytes)
+ fingerprint := hex.EncodeToString(hash.Sum(nil))
+
+ // Return the fingerprint.
+ vm.PushString(fingerprint)
+ return 1
+}
+
+// getCertificate gets the certificate used to submit the transaction.
+func (identityService *IdentityService) getCertificate(vm *duktape.Context) (result int) {
+ logger.Debug("Entering IdentityService.getCertificate", vm)
+ defer func() { logger.Debug("Exiting IdentityService.getCertificate", result) }()
+
+ // Return the certificate.
+ vm.PushString(identityService.PEM)
return 1
}
diff --git a/packages/composer-runtime/lib/compiledscriptbundle.js b/packages/composer-runtime/lib/compiledscriptbundle.js
index dc71e16950..79e291fa87 100644
--- a/packages/composer-runtime/lib/compiledscriptbundle.js
+++ b/packages/composer-runtime/lib/compiledscriptbundle.js
@@ -51,8 +51,9 @@ class CompiledScriptBundle {
// If we didn't find any functions to call, then throw an error!
if (functionNames.length === 0) {
- LOG.error(`Could not find any functions to execute for transaction ${resolvedTransaction.getFullyQualifiedIdentifier()}`);
- throw new Error(`Could not find any functions to execute for transaction ${resolvedTransaction.getFullyQualifiedIdentifier()}`);
+ const error = new Error(`Could not find any functions to execute for transaction ${resolvedTransaction.getFullyQualifiedIdentifier()}`);
+ LOG.error(method, error);
+ throw error;
}
// Generate an instance of the compiled script bundle.
diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js
index 88396ae600..3e8b264432 100644
--- a/packages/composer-runtime/lib/context.js
+++ b/packages/composer-runtime/lib/context.js
@@ -97,17 +97,19 @@ class Context {
*/
constructor(engine) {
this.engine = engine;
+ this.function = null;
+ this.arguments = null;
this.businessNetworkDefinition = null;
this.registryManager = null;
this.resolver = null;
this.api = null;
this.queryExecutor = null;
this.identityManager = null;
+ this.identity = null;
this.participant = null;
this.transaction = null;
this.accessController = null;
this.sysregistries = null;
- this.sysidentities = null;
this.eventNumber = 0;
this.scriptCompiler = null;
this.compiledScriptBundle = null;
@@ -117,6 +119,22 @@ class Context {
this.compiledAclBundle = null;
}
+ /**
+ * Get the name of the currently executing runtime method.
+ * @return {string} The name of the currently executing runtime method.
+ */
+ getFunction() {
+ return this.function;
+ }
+
+ /**
+ * Get the arguments for the currently executing runtime method.
+ * @return {string} The arguments for the currently executing runtime method.
+ */
+ getArguments() {
+ return this.arguments;
+ }
+
/**
* Load the business network record from the world state.
* @return {Promise} A promise that will be resolved with the business network record
@@ -135,7 +153,7 @@ class Context {
if (object.undeployed){
throw new Error('The business network has been undeployed');
}
- LOG.exit(object);
+ LOG.exit(method, object);
return object;
});
}
@@ -267,26 +285,52 @@ class Context {
loadCurrentParticipant() {
const method = 'loadCurrentParticipant';
LOG.entry(method);
- let currentUserID = this.getIdentityService().getCurrentUserID();
- LOG.debug(method, 'Got current user ID', currentUserID);
- if (currentUserID) {
- return this.getIdentityManager().getParticipant(currentUserID)
- .then((participant) => {
- LOG.debug(method, 'Found current participant', participant.getFullyQualifiedIdentifier());
- LOG.exit(method, participant);
- return participant;
- })
- .catch((error) => {
- LOG.error(method, 'Could not find current participant', error);
- throw new Error(`Could not determine the participant for identity '${currentUserID}'. The identity may be invalid or may have been revoked.`);
- });
- } else {
- // TODO: this is temporary whilst we migrate to requiring all
- // users to have identities that are mapped to participants.
- LOG.debug(method, 'Could not determine current user ID');
- LOG.exit(method, null);
- return Promise.resolve(null);
- }
+
+ // Load the current identity.
+ return this.getIdentityManager().getIdentity()
+ .then((identity) => {
+
+ // Save the current identity.
+ this.identity = identity;
+
+ // Validate the identity.
+ try {
+ this.getIdentityManager().validateIdentity(identity);
+ } catch (e) {
+
+ // Check for the case of activation required, and the user is trying to activate.
+ if (e.activationRequired && this.getFunction() === 'activateIdentity') {
+ // Ignore.
+ } else {
+ throw e;
+ }
+
+ }
+
+ // Load the current participant.
+ return this.getIdentityManager().getParticipant(identity);
+
+ })
+ .then((participant) => {
+ LOG.exit(method, participant);
+ return participant;
+ })
+ .catch((error) => {
+
+ // Check for an admin user.
+ // TODO: this is temporary whilst we migrate to requiring all
+ // users to have identities that are mapped to participants.
+ const name = this.getIdentityService().getName();
+ if (name && name.match(/admin/i)) {
+ LOG.exit(method, null);
+ return null;
+ }
+
+ // Throw the error.
+ LOG.error(method, error);
+ throw error;
+
+ });
}
/**
@@ -415,12 +459,13 @@ class Context {
/**
* Initialize the context for use.
* @param {Object} [options] The options to use.
+ * @param {string} [options.function] The name of the currently executing runtime method.
+ * @param {string} [options.arguments] The arguments for the currently executing runtime method.
* @param {BusinessNetworkDefinition} [options.businessNetworkDefinition] The business network definition to use.
* @param {CompiledScriptBundle} [options.compiledScriptBundle] The compiled script bundle to use.
+ * @param {DataCollection} [options.sysregistries] The system registries collection to use.
* @param {boolean} [options.reinitialize] Set to true if being reinitialized as a result of an upgrade to the
* business network, falsey value if not.
- * @param {DataCollection} [options.sysregistries] The system registries collection to use.
- * @param {DataCollection} [options.sysidentities] The system identities collection to use.
* @return {Promise} A promise that will be resolved when complete, or rejected
* with an error.
*/
@@ -428,6 +473,8 @@ class Context {
const method = 'initialize';
LOG.entry(method, options);
options = options || {};
+ this.function = options.function || this.function;
+ this.arguments = options.arguments || this.arguments;
return Promise.resolve()
.then(() => {
return this.findBusinessNetworkDefinition(options);
@@ -460,17 +507,6 @@ class Context {
});
}
})
- .then(() => {
- LOG.debug(method, 'Loading sysidentities collection', options.sysidentities);
- if (options.sysidentities) {
- this.sysidentities = options.sysidentities;
- } else {
- return this.getDataService().getCollection('$sysidentities')
- .then((sysidentities) => {
- this.sysidentities = sysidentities;
- });
- }
- })
.then(() => {
LOG.debug(method, 'Loading current participant');
return this.loadCurrentParticipant();
@@ -654,7 +690,7 @@ class Context {
*/
getIdentityManager() {
if (!this.identityManager) {
- this.identityManager = new IdentityManager(this.getDataService(), this.getRegistryManager(), this.getSystemIdentities());
+ this.identityManager = new IdentityManager(this);
}
return this.identityManager;
}
@@ -722,17 +758,6 @@ class Context {
return this.sysregistries;
}
- /**
- * Get the system identities collection.
- * @return {DataCollection} The system registries collection.
- */
- getSystemIdentities() {
- if (!this.sysidentities) {
- throw new Error('must call initialize before calling this function');
- }
- return this.sysidentities;
- }
-
/**
* Get the next event number
* @return {integer} the event number.
diff --git a/packages/composer-runtime/lib/engine.identities.js b/packages/composer-runtime/lib/engine.identities.js
index 2b24dfecb3..a4479a05e8 100644
--- a/packages/composer-runtime/lib/engine.identities.js
+++ b/packages/composer-runtime/lib/engine.identities.js
@@ -27,46 +27,89 @@ const LOG = Logger.getLog('EngineIdentities');
class EngineIdentities {
/**
- * Add a mapping from the specified identity, or user ID, to the specified
- * participant.
+ * Issue a new identity to a participant in the business network.
* @param {Context} context The request context.
* @param {string[]} args The arguments to pass to the chaincode function.
* @return {Promise} A promise that will be resolved when complete, or rejected
* with an error.
*/
- addParticipantIdentity(context, args) {
- const method = 'addParticipantIdentity';
+ issueIdentity(context, args) {
+ const method = 'issueIdentity';
LOG.entry(method, context, args);
if (args.length !== 2) {
LOG.error(method, 'Invalid arguments', args);
- throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'addParticipantIdentity', ['participantId', 'userId']));
+ throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'issueIdentity', ['participantFQI', 'identityName']));
}
- let participantId = args[0];
- let userId = args[1];
+ let participantFQI = args[0];
+ let identityName = args[1];
let identityManager = context.getIdentityManager();
- return identityManager.addIdentityMapping(participantId, userId)
+ return identityManager.issueIdentity(participantFQI, identityName)
.then(() => {
LOG.exit(method);
});
}
/**
- * Remove any mapping for the specified identity, or user ID.
+ * Bind an existing identity to a participant in the business network.
* @param {Context} context The request context.
* @param {string[]} args The arguments to pass to the chaincode function.
* @return {Promise} A promise that will be resolved when complete, or rejected
* with an error.
*/
- removeIdentity(context, args) {
- const method = 'removeIdentity';
+ bindIdentity(context, args) {
+ const method = 'bindIdentity';
+ LOG.entry(method, context, args);
+ if (args.length !== 2) {
+ LOG.error(method, 'Invalid arguments', args);
+ throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'bindIdentity', ['participantFQI', 'certificate']));
+ }
+ let participantFQI = args[0];
+ let certificate = args[1];
+ let identityManager = context.getIdentityManager();
+ return identityManager.bindIdentity(participantFQI, certificate)
+ .then(() => {
+ LOG.exit(method);
+ });
+ }
+
+ /**
+ * Activate the current identity in the business network.
+ * @param {Context} context The request context.
+ * @param {string[]} args The arguments to pass to the chaincode function.
+ * @return {Promise} A promise that will be resolved when complete, or rejected
+ * with an error.
+ */
+ activateIdentity(context, args) {
+ const method = 'activateIdentity';
+ LOG.entry(method, context, args);
+ if (args.length !== 0) {
+ LOG.error(method, 'Invalid arguments', args);
+ throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'activateIdentity', []));
+ }
+ let identityManager = context.getIdentityManager();
+ return identityManager.activateIdentity()
+ .then(() => {
+ LOG.exit(method);
+ });
+ }
+
+ /**
+ * Revoke an identity in the business network.
+ * @param {Context} context The request context.
+ * @param {string[]} args The arguments to pass to the chaincode function.
+ * @return {Promise} A promise that will be resolved when complete, or rejected
+ * with an error.
+ */
+ revokeIdentity(context, args) {
+ const method = 'revokeIdentity';
LOG.entry(method, context, args);
if (args.length !== 1) {
LOG.error(method, 'Invalid arguments', args);
- throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'removeIdentity', ['userId']));
+ throw new Error(util.format('Invalid arguments "%j" to function "%s", expecting "%j"', args, 'revokeIdentity', ['identityId']));
}
- let userId = args[0];
+ let identityId = args[0];
let identityManager = context.getIdentityManager();
- return identityManager.removeIdentityMapping(userId)
+ return identityManager.revokeIdentity(identityId)
.then(() => {
LOG.exit(method);
});
diff --git a/packages/composer-runtime/lib/engine.js b/packages/composer-runtime/lib/engine.js
index 9cc9d37725..fed4d950ef 100644
--- a/packages/composer-runtime/lib/engine.js
+++ b/packages/composer-runtime/lib/engine.js
@@ -105,7 +105,7 @@ class Engine {
let dataService = context.getDataService();
let businessNetworkBase64, businessNetworkHash, businessNetworkRecord, businessNetworkDefinition;
let compiledScriptBundle, compiledQueryBundle, compiledAclBundle;
- let sysregistries, sysidentities;
+ let sysregistries;
return Promise.resolve()
.then(() => {
@@ -175,28 +175,19 @@ class Engine {
sysregistries = sysregistries_;
});
- })
- .then(() => {
-
- // Ensure that the system identities collection exists.
- LOG.debug(method, 'Ensuring that sysidentities collection exists');
- return dataService.ensureCollection('$sysidentities')
- .then((sysidentities_) => {
- sysidentities = sysidentities_;
- });
-
})
.then(() => {
// Initialize the context.
LOG.debug(method, 'Initializing context');
return context.initialize({
+ function: fcn,
+ arguments: args,
businessNetworkDefinition: businessNetworkDefinition,
compiledScriptBundle: compiledScriptBundle,
compiledQueryBundle: compiledQueryBundle,
compiledAclBundle: compiledAclBundle,
- sysregistries: sysregistries,
- sysidentities: sysidentities
+ sysregistries: sysregistries
});
})
@@ -270,7 +261,7 @@ class Engine {
LOG.entry(method, context, fcn, args);
if (this[fcn]) {
LOG.debug(method, 'Initializing context');
- return context.initialize()
+ return context.initialize({ function: fcn, arguments: args })
.then(() => {
return context.transactionStart(false);
})
@@ -341,7 +332,7 @@ class Engine {
LOG.entry(method, context, fcn, args);
if (this[fcn]) {
LOG.debug(method, 'Initializing context');
- return context.initialize()
+ return context.initialize({ function: fcn, arguments: args })
.then(() => {
return context.transactionStart(true);
})
diff --git a/packages/composer-runtime/lib/engine.logging.js b/packages/composer-runtime/lib/engine.logging.js
index 5ad4b698db..34f54b4a29 100644
--- a/packages/composer-runtime/lib/engine.logging.js
+++ b/packages/composer-runtime/lib/engine.logging.js
@@ -63,7 +63,7 @@ class EngineLogging {
}
if (context.getParticipant() === null) {
const curLogLevel = this.getContainer().getLoggingService().getLogLevel();
- LOG.debug('current log level=' + curLogLevel);
+ LOG.debug(method, 'current log level=' + curLogLevel);
return Promise.resolve(curLogLevel);
}
throw new Error('Authorization failure');
diff --git a/packages/composer-runtime/lib/httpservice.js b/packages/composer-runtime/lib/httpservice.js
index 77097e9e8a..64d3d2bb8c 100644
--- a/packages/composer-runtime/lib/httpservice.js
+++ b/packages/composer-runtime/lib/httpservice.js
@@ -53,7 +53,7 @@ class HTTPService extends Service {
else {
response = responseThing;
}
- LOG.info('Reponse from URL ' + url, JSON.stringify(response));
+ LOG.info(method, 'Reponse from URL ' + url, JSON.stringify(response));
if(response.statusCode >= 200 && response.statusCode < 300) {
if(response.body && typeof response.body === 'string') {
@@ -61,13 +61,13 @@ class HTTPService extends Service {
response.body = JSON.parse(response.body);
}
catch(err) {
- LOG.warn('Body data could not be converted to JS object', response.body);
+ LOG.warn(method, 'Body data could not be converted to JS object', response.body);
}
}
return Promise.resolve(response);
}
else {
- LOG.error('Error statusCode ', response.statusCode);
+ LOG.error(method, 'Error statusCode ', response.statusCode);
return Promise.reject(JSON.stringify(response));
}
})
diff --git a/packages/composer-runtime/lib/identitymanager.js b/packages/composer-runtime/lib/identitymanager.js
index 2aae9ee7f2..fb3eb4ce94 100644
--- a/packages/composer-runtime/lib/identitymanager.js
+++ b/packages/composer-runtime/lib/identitymanager.js
@@ -14,8 +14,9 @@
'use strict';
+const createHash = require('sha.js');
const Logger = require('composer-common').Logger;
-const Resource = require('composer-common').Resource;
+const ModelUtil = require('composer-common').ModelUtil;
const LOG = Logger.getLog('IdentityManager');
@@ -27,61 +28,241 @@ class IdentityManager {
/**
* Constructor.
- * @param {DataService} dataService The data service to use.
- * @param {RegistryManager} registryManager The registry manager to use.
- * @param {DataCollection} sysidentities The system identities collection.
+ * @param {Context} context The request context.
*/
- constructor(dataService, registryManager, sysidentities) {
- this.dataService = dataService;
- this.registryManager = registryManager;
- this.sysidentities = sysidentities;
+ constructor(context) {
+ this.identityService = context.getIdentityService();
+ this.registryManager = context.getRegistryManager();
+ this.factory = context.getFactory();
}
/**
- * Add a new mapping for the specified identity (user ID) to the specified
- * participant.
- * @param {(Resource|string)} participant The participant, or the unique
- * identifier of the participant.
- * @param {string} userID The identity (user ID) to map to the participant.
- * @return {Promise} A promise that is resolved when a new mapping for the
- * specified identity has been created.
+ * Get the identity registry.
+ * @return {Promise} A promise that will be resolved with an {@link Registry}
+ * when complete, or rejected with an error.
*/
- addIdentityMapping(participant, userID) {
- const method = 'addIdentityMapping';
- LOG.entry(method, participant, userID);
- let participantFQI, participantFQT, participantID;
- if (participant instanceof Resource) {
- participantFQI = participant.getFullyQualifiedIdentifier();
- participantFQT = participant.getFullyQualifiedType();
- participantID = participant.getIdentifier();
- } else {
- participantFQI = participant;
- let hashIndex = participantFQI.indexOf('#');
- if (hashIndex === -1) {
- throw new Error('Invalid fully qualified participant identifier');
- }
- participantFQT = participantFQI.substring(0, hashIndex);
- participantID = participantFQI.substring(hashIndex + 1);
+ getIdentityRegistry() {
+ const method = 'getIdentityRegistry';
+ LOG.entry(method);
+ return this.registryManager.get('Asset', 'org.hyperledger.composer.system.Identity')
+ .then((identityRegistry) => {
+ LOG.exit(method, identityRegistry);
+ return identityRegistry;
+ });
+ }
+
+ /**
+ * Find the identity in the identity registry that maps to the certificate that
+ * was used to sign and submit the current transaction.
+ * @return {Promise} A promise that will be resolved with a {@link Resource}
+ * when complete, or rejected with an error.
+ */
+ getIdentity() {
+ const method = 'getIdentity';
+ LOG.entry(method);
+ let identityRegistry, identifier;
+ return this.getIdentityRegistry()
+ .then((identityRegistry_) => {
+
+ // Check to see if the identity exists.
+ identityRegistry = identityRegistry_;
+ identifier = this.identityService.getIdentifier();
+ return identityRegistry.exists(identifier);
+
+ })
+ .then((exists) => {
+
+ // If it doesn't exist, then try again with the temporary identifier, which is hash(name, issuer).
+ if (!exists) {
+ const sha256 = createHash('sha256');
+ const name = this.identityService.getName();
+ const issuer = this.identityService.getIssuer();
+ sha256.update(name, 'utf8');
+ sha256.update(issuer, 'utf8');
+ identifier = sha256.digest('hex');
+ return identityRegistry.exists(identifier);
+ } else {
+ return exists;
+ }
+
+ })
+ .then((exists) => {
+
+ // If it still doesn't exist, throw!
+ if (!exists) {
+ const error = new Error('The current identity has not been registered');
+ LOG.error(method, error);
+ throw error;
+ }
+
+ // Get the identity.
+ return identityRegistry.get(identifier);
+
+ })
+ .then((identity) => {
+ LOG.exit(method, identity);
+ return identity;
+ });
+ }
+
+ /**
+ * Validate the specified identity to confirm that it is valid for use with the
+ * business network. We validate that the identity is not revoked, pending activation,
+ * or in an invalid state.
+ * @param {Resource} identity The identity to validate.
+ */
+ validateIdentity(identity) {
+ const method = 'validateIdentity';
+ LOG.entry(method, identity);
+
+ // Check for a revoked identity.
+ if (identity.state === 'REVOKED') {
+ const error = new Error('The current identity has been revoked');
+ LOG.error(method, error);
+ throw error;
}
- LOG.debug(method, 'Looking for participant registry', participantFQT);
+
+ // Check for an issued or bound identity, in which case activation is required.
+ if (identity.state === 'ISSUED' || identity.state === 'BOUND') {
+ const error = new Error('The current identity must be activated (ACTIVATION_REQUIRED)');
+ error.activationRequired = true;
+ LOG.error(method, error);
+ throw error;
+ }
+
+ // Ensure that the identity is activated.
+ if (identity.state !== 'ACTIVATED') {
+ const error = new Error('The current identity is in an unknown state: ' + identity.state);
+ LOG.error(method, error);
+ throw error;
+ }
+
+ LOG.exit(method);
+ }
+
+ /**
+ * Find the participant for the specified identity.
+ * @param {Resource} identity The identity to find the participant for.
+ * @return {Promise} A promise that will be resolved with a {@link Resource}
+ * when complete, or rejected with an error.
+ */
+ getParticipant(identity) {
+ const method = 'getParticipant';
+ LOG.entry(method);
+ const participant = identity.participant;
+ const participantFQT = participant.getFullyQualifiedType();
return this.registryManager.get('Participant', participantFQT)
.then((participantRegistry) => {
- LOG.debug(method, 'Found participant registry, looking for participant', participantID);
- return participantRegistry.get(participantID);
+ return participantRegistry.get(participant.getIdentifier());
})
.then((participant) => {
- LOG.debug(method, 'Got $sysidentities collection, checking for existing mapping');
- return this.sysidentities.exists(userID);
+ LOG.exit(method, participant);
+ return participant;
})
- .then((exists) => {
- if (exists) {
- LOG.error(method, 'Found an existing mapping for user ID', userID);
- throw new Error(`Found an existing mapping for user ID '${userID}'`);
- }
- LOG.debug(method, 'No existing mapping exists for user ID, adding');
- return this.sysidentities.add(userID, {
- participant: participantFQI
+ .catch(() => {
+ const error = new Error('The current identity is bound to a participant that does not exist');
+ LOG.error(method, error);
+ throw error;
+ });
+ }
+
+ /**
+ * Parse the fully qualified participant identifier into a relationship to that participant.
+ * @param {string} participantFQI The fully qualified participant identifier.
+ * @return {Relationship} A relationship to the specified participant.
+ */
+ parseParticipant(participantFQI) {
+ const method = 'parseParticipant';
+ LOG.entry(method, participantFQI);
+ const hashIndex = participantFQI.indexOf('#');
+ if (hashIndex === -1) {
+ throw new Error('Invalid fully qualified participant identifier');
+ }
+ const participantFQT = participantFQI.substring(0, hashIndex);
+ const participantNS = ModelUtil.getNamespace(participantFQT);
+ const participantType = ModelUtil.getShortName(participantFQT);
+ const participantID = participantFQI.substring(hashIndex + 1);
+ const participant = this.factory.newRelationship(participantNS, participantType, participantID);
+ LOG.exit(method, participant);
+ return participant;
+ }
+
+ /**
+ * Issue a new identity to a participant in the business network.
+ * @param {string} participantFQI The fully qualified participant identifier.
+ * @param {string} identityName The name of the new identity.
+ * @return {Promise} A promise that will be resolved when complete, or rejected
+ * with an error.
+ */
+ issueIdentity(participantFQI, identityName) {
+ const method = 'issueIdentity';
+ LOG.entry(method, participantFQI, identityName);
+ return this.getIdentityRegistry()
+ .then((identityRegistry) => {
+
+ // Create the temporary identifier, which is hash(name, issuer)
+ const sha256 = createHash('sha256');
+ const issuer = this.identityService.getIssuer();
+ sha256.update(identityName, 'utf8');
+ sha256.update(issuer, 'utf8');
+ const identifier = sha256.digest('hex');
+
+ // Parse the participant into a relationship.
+ const participant = this.parseParticipant(participantFQI);
+
+ // Create the new identity and add it to the identity registry.
+ const identity = this.factory.newResource('org.hyperledger.composer.system', 'Identity', identifier);
+ Object.assign(identity, {
+ name: identityName,
+ issuer,
+ certificate: '',
+ state: 'ISSUED',
+ participant
+ });
+ return identityRegistry.add(identity);
+
+ })
+ .then(() => {
+ LOG.exit(method);
+ });
+ }
+
+ /**
+ * Bind an existing identity to a participant in the business network.
+ * @param {string} participantFQI The fully qualified participant identifier.
+ * @param {string} certificate The certificate for the existing identity.
+ * @return {Promise} A promise that will be resolved when complete, or rejected
+ * with an error.
+ */
+ bindIdentity(participantFQI, certificate) {
+ const method = 'bindIdentity';
+ LOG.entry(method, participantFQI, certificate);
+ return this.getIdentityRegistry()
+ .then((identityRegistry) => {
+
+ // Parse the certificate into a byte array.
+ const bytes = certificate
+ .replace(/-----BEGIN CERTIFICATE-----/, '')
+ .replace(/-----END CERTIFICATE-----/, '')
+ .replace(/[\r\n]+/g, '');
+ const buffer = Buffer.from(bytes, 'base64');
+ const sha256 = createHash('sha256');
+ const identityId = sha256.update(buffer).digest('hex');
+
+ // Parse the participant into a relationship.
+ const participant = this.parseParticipant(participantFQI);
+
+ // Create the new identity and add it to the identity registry.
+ const identity = this.factory.newResource('org.hyperledger.composer.system', 'Identity', identityId);
+ Object.assign(identity, {
+ name: '',
+ issuer: '',
+ certificate,
+ state: 'BOUND',
+ participant
});
+ return identityRegistry.add(identity);
+
})
.then(() => {
LOG.exit(method);
@@ -89,23 +270,34 @@ class IdentityManager {
}
/**
- * Remove an existing mapping for the specified identity (user ID) to a
- * participant.
- * @param {string} userID The identity (user ID).
- * @return {Promise} A promise that is resolved when a new mapping for the
- * specified identity has been created.
+ * Activate the current identity in the business network.
+ * @return {Promise} A promise that will be resolved when complete, or rejected
+ * with an error.
*/
- removeIdentityMapping(userID) {
- const method = 'removeIdentityMapping';
- LOG.entry(method, userID);
- LOG.debug(method, 'Got $sysidentities collection, checking for existing mapping');
- return this.sysidentities.exists(userID)
- .then((exists) => {
- if (!exists) {
- LOG.debug('No existing mapping exists for user ID, ignoring');
- return;
+ activateIdentity() {
+ const method = 'activateIdentity';
+ LOG.entry(method);
+ let identityRegistry;
+ return this.getIdentityRegistry()
+ .then((identityRegistry_) => {
+ identityRegistry = identityRegistry_;
+ return this.getIdentity();
+ })
+ .then((identity) => {
+
+ // If the identity has been issued, we must delete it and then create a new one.
+ if (identity.state === 'ISSUED') {
+ return this.activateIssuedIdentity(identityRegistry, identity);
}
- return this.sysidentities.remove(userID);
+
+ // If the identity has been bound, then we can update it.
+ if (identity.state === 'BOUND') {
+ return this.activateBoundIdentity(identityRegistry, identity);
+ }
+
+ // Shouldn't get here.
+ throw new Error('The current identity cannot be activated because it is in an unknown state: ' + identity.state);
+
})
.then(() => {
LOG.exit(method);
@@ -113,37 +305,100 @@ class IdentityManager {
}
/**
- * Retrieve the participant for the specified identity (user ID).
- * @param {string} userID The identity (user ID).
- * @return {Promise} A promise that is resolved with a {@link Resource}
- * representing the participant, or rejected with an error.
+ * Activate the specified identity (in the ISSUED state) in the business network.
+ * @param {Registry} identityRegistry The identity registry.
+ * @param {Resource} identity The identity to activate.
+ * @return {Promise} A promise that will be resolved when complete, or rejected
+ * with an error.
*/
- getParticipant(userID) {
- const method = 'getParticipant';
- LOG.entry(method, userID);
- LOG.debug(method, 'Getting $sysidentities collection');
- let participantFQI, participantFQT, participantID;
- LOG.debug(method, 'Got $sysidentities collection, checking for existing mapping');
- return this.sysidentities.get(userID)
- .then((mapping) => {
- participantFQI = mapping.participant;
- LOG.debug(method, 'Found mapping, participant is', participantFQI);
- let hashIndex = participantFQI.indexOf('#');
- if (hashIndex === -1) {
- throw new Error('Invalid fully qualified participant identifier');
- }
- participantFQT = participantFQI.substring(0, hashIndex);
- participantID = participantFQI.substring(hashIndex + 1);
- LOG.debug(method, 'Looking for participant registry', participantFQT);
- return this.registryManager.get('Participant', participantFQT);
+ activateIssuedIdentity(identityRegistry, identity) {
+ const method = 'activateIssuedIdentity';
+ LOG.entry(method, identityRegistry, identity);
+
+ // Grab information from the certificate.
+ const identifier = this.identityService.getIdentifier();
+ const name = this.identityService.getName();
+ const issuer = this.identityService.getIssuer();
+ const certificate = this.identityService.getCertificate();
+
+ // Validate the issuer to check it matches the issuer of the identity.
+ if (identity.issuer !== issuer) {
+ throw new Error('The current identity cannot be activated because the issuer is invalid');
+ }
+
+ // Create the new identity.
+ const newIdentity = this.factory.newResource('org.hyperledger.composer.system', 'Identity', identifier);
+ Object.assign(newIdentity, {
+ name,
+ issuer,
+ certificate,
+ state: 'ACTIVATED',
+ participant: identity.participant
+ });
+
+ // Remove the old identity and add the new identity into the identity registry.
+ return identityRegistry.remove((identity))
+ .then(() => {
+ return identityRegistry.add(newIdentity);
})
- .then((participantRegistry) => {
- LOG.debug(method, 'Found participant registry, looking for participant', participantID);
- return participantRegistry.get(participantID);
+ .then(() => {
+ LOG.exit(method);
+ });
+ }
+
+ /**
+ * Activate the specified identity (in the BOUND state) in the business network.
+ * @param {Registry} identityRegistry The identity registry.
+ * @param {Resource} identity The identity to activate.
+ * @return {Promise} A promise that will be resolved when complete, or rejected
+ * with an error.
+ */
+ activateBoundIdentity(identityRegistry, identity) {
+ const method = 'activateBoundIdentity';
+ LOG.entry(method, identityRegistry, identity);
+
+ // Grab information from the certificate.
+ const name = this.identityService.getName();
+ const issuer = this.identityService.getIssuer();
+
+ // Update the identity and update it in the identity registry.
+ Object.assign(identity, {
+ name,
+ issuer,
+ state: 'ACTIVATED'
+ });
+ return identityRegistry.update(identity)
+ .then(() => {
+ LOG.exit(method);
+ });
+ }
+
+ /**
+ * Revoke an identity in the business network.
+ * @param {string} identityId The identifier of the identity.
+ * @return {Promise} A promise that will be resolved when complete, or rejected
+ * with an error.
+ */
+ revokeIdentity(identityId) {
+ const method = 'revokeIdentity';
+ LOG.entry(method, identityId);
+ let identityRegistry;
+ return this.getIdentityRegistry()
+ .then((identityRegistry_) => {
+ identityRegistry = identityRegistry_;
+ return identityRegistry.get(identityId);
})
- .then((participant) => {
- LOG.exit(method, participant);
- return participant;
+ .then((identity) => {
+
+ // Revoke the identity and updaye it in the identity registry.
+ Object.assign(identity, {
+ state: 'REVOKED'
+ });
+ return identityRegistry.update(identity);
+
+ })
+ .then(() => {
+ LOG.exit(method);
});
}
diff --git a/packages/composer-runtime/lib/identityservice.js b/packages/composer-runtime/lib/identityservice.js
index f18d74f6b4..5bc7037f8e 100644
--- a/packages/composer-runtime/lib/identityservice.js
+++ b/packages/composer-runtime/lib/identityservice.js
@@ -25,12 +25,38 @@ const Service = require('./service');
class IdentityService extends Service {
/**
- * Retrieve the current user ID.
+ * Get a unique identifier for the identity used to submit the transaction.
* @abstract
- * @return {string} The current user ID, or null if the current user ID cannot
- * be determined or has not been specified.
+ * @return {string} A unique identifier for the identity used to submit the transaction.
*/
- getCurrentUserID() {
+ getIdentifier() {
+ throw new Error('abstract function called');
+ }
+
+ /**
+ * Get the name of the identity used to submit the transaction.
+ * @abstract
+ * @return {string} The name of the identity used to submit the transaction.
+ */
+ getName() {
+ throw new Error('abstract function called');
+ }
+
+ /**
+ * Get the issuer of the identity used to submit the transaction.
+ * @abstract
+ * @return {string} The issuer of the identity used to submit the transaction.
+ */
+ getIssuer() {
+ throw new Error('abstract function called');
+ }
+
+ /**
+ * Get the certificate for the identity used to submit the transaction.
+ * @abstract
+ * @return {string} The certificate for the identity used to submit the transaction.
+ */
+ getCertificate() {
throw new Error('abstract function called');
}
diff --git a/packages/composer-runtime/lib/queryexecutor.js b/packages/composer-runtime/lib/queryexecutor.js
index 0cf90f9c34..84c5118151 100644
--- a/packages/composer-runtime/lib/queryexecutor.js
+++ b/packages/composer-runtime/lib/queryexecutor.js
@@ -305,7 +305,7 @@ class QueryExecutor {
foundRelationships.forEach((foundRelationship) => {
LOG.debug(method, 'Found relationship object', foundRelationship.relationship.toString());
this.modifyRelationship(foundRelationship.relationship, (name) => {
- LOG.debug('Relationship accessed', foundRelationship.relationship.toString());
+ LOG.debug(method, 'Relationship accessed', foundRelationship.relationship.toString());
return this.resolver.resolveRelationship(foundRelationship.relationship, {
cachedResources: new Map(),
skipRecursion: true
diff --git a/packages/composer-runtime/lib/transactionlogger.js b/packages/composer-runtime/lib/transactionlogger.js
index 65c8cb6e44..c5d72360b5 100644
--- a/packages/composer-runtime/lib/transactionlogger.js
+++ b/packages/composer-runtime/lib/transactionlogger.js
@@ -53,7 +53,7 @@ class TransactionLogger {
onResourceAdded(event) {
const method = 'onResourceAdded';
LOG.entry(method, event);
- LOG.exit();
+ LOG.exit(method);
}
/**
@@ -78,7 +78,7 @@ class TransactionLogger {
let patches = jsonpatch.compare(oldJSON, newJSON);
LOG.debug(method, 'Generated JSON Patch', patches);
- LOG.exit();
+ LOG.exit(method);
}
/**
@@ -88,7 +88,7 @@ class TransactionLogger {
onResourceRemoved(event) {
const method = 'onResourceRemoved';
LOG.entry(method, event);
- LOG.exit();
+ LOG.exit(method);
}
}
diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js
index 8adb883ad2..60344d2f5a 100644
--- a/packages/composer-runtime/test/context.js
+++ b/packages/composer-runtime/test/context.js
@@ -75,6 +75,24 @@ describe('Context', () => {
});
+ describe('#getFunction', () => {
+
+ it('should return the current function', () => {
+ context.function = 'suchfunc';
+ context.getFunction().should.equal('suchfunc');
+ });
+
+ });
+
+ describe('#getArguments', () => {
+
+ it('should return the current arguments', () => {
+ context.arguments = ['arg1', 'arg2'];
+ context.getArguments().should.deep.equal(['arg1', 'arg2']);
+ });
+
+ });
+
describe('#loadBusinessNetworkRecord', () => {
it('should load the business record', () => {
@@ -259,41 +277,104 @@ describe('Context', () => {
describe('#loadCurrentParticipant', () => {
- it('should return null if no identity is specified', () => {
- let mockIdentityService = sinon.createStubInstance(IdentityService);
- sandbox.stub(context, 'getIdentityService').returns(mockIdentityService);
- let mockIdentityManager = sinon.createStubInstance(IdentityManager);
- sandbox.stub(context, 'getIdentityManager').returns(mockIdentityManager);
- mockIdentityService.getCurrentUserID.returns(null);
- let mockParticipant = sinon.createStubInstance(Resource);
- mockParticipant.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1');
- mockIdentityManager.getParticipant.withArgs('dogeid1').resolves(mockParticipant);
+ let mockIdentityManager;
+ let mockIdentityService;
+ let mockIdentity;
+ let mockParticipant;
+
+ beforeEach(() => {
+ mockIdentityManager = sinon.createStubInstance(IdentityManager);
+ sinon.stub(context, 'getIdentityManager').returns(mockIdentityManager);
+ mockIdentityService = sinon.createStubInstance(IdentityService);
+ sinon.stub(context, 'getIdentityService').returns(mockIdentityService);
+ mockIdentity = sinon.createStubInstance(Resource);
+ mockParticipant = sinon.createStubInstance(Resource);
+ });
+
+ it('should get the identity, validate it, and get the participant', () => {
+ mockIdentityManager.getIdentity.resolves(mockIdentity);
+ mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant);
return context.loadCurrentParticipant()
- .should.eventually.be.equal(null);
+ .should.eventually.be.equal(mockParticipant)
+ .then(() => {
+ sinon.assert.calledOnce(mockIdentityManager.validateIdentity);
+ sinon.assert.calledWith(mockIdentityManager.validateIdentity, mockIdentity);
+ });
});
- it('should load the current participant if an identity is specified', () => {
- let mockIdentityService = sinon.createStubInstance(IdentityService);
- sandbox.stub(context, 'getIdentityService').returns(mockIdentityService);
- let mockIdentityManager = sinon.createStubInstance(IdentityManager);
- sandbox.stub(context, 'getIdentityManager').returns(mockIdentityManager);
- mockIdentityService.getCurrentUserID.returns('dogeid1');
- let mockParticipant = sinon.createStubInstance(Resource);
- mockParticipant.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1');
- mockIdentityManager.getParticipant.withArgs('dogeid1').resolves(mockParticipant);
+ it('should ignore an activation required message when calling activate identity', () => {
+ mockIdentityManager.getIdentity.resolves(mockIdentity);
+ mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant);
+ context.function = 'activateIdentity';
+ const error = new Error('such error');
+ error.activationRequired = true;
+ mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(error);
return context.loadCurrentParticipant()
- .should.eventually.be.equal(mockParticipant);
+ .should.eventually.be.equal(mockParticipant)
+ .then(() => {
+ sinon.assert.calledOnce(mockIdentityManager.validateIdentity);
+ sinon.assert.calledWith(mockIdentityManager.validateIdentity, mockIdentity);
+ });
});
- it('should throw an error if an invalid identity is specified', () => {
- let mockIdentityService = sinon.createStubInstance(IdentityService);
- sandbox.stub(context, 'getIdentityService').returns(mockIdentityService);
- let mockIdentityManager = sinon.createStubInstance(IdentityManager);
- sandbox.stub(context, 'getIdentityManager').returns(mockIdentityManager);
- mockIdentityService.getCurrentUserID.returns('dogeid1');
- mockIdentityManager.getParticipant.withArgs('dogeid1').rejects(new Error('no such participant'));
+ it('should throw an activation required message when calling another function', () => {
+ mockIdentityManager.getIdentity.resolves(mockIdentity);
+ mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant);
+ context.function = 'bindIdentity';
+ const error = new Error('such error');
+ error.activationRequired = true;
+ mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(error);
+ return context.loadCurrentParticipant()
+ .should.be.rejectedWith(/such error/);
+ });
+
+ it('should throw an non-activation required message', () => {
+ mockIdentityManager.getIdentity.resolves(mockIdentity);
+ mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant);
+ mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(new Error('such error'));
return context.loadCurrentParticipant()
- .should.be.rejectedWith(/The identity may be invalid or may have been revoked/);
+ .should.be.rejectedWith(/such error/);
+ });
+
+ it('should ignore any errors from looking up the identity for admin users', () => {
+ let promise = Promise.resolve();
+ ['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => {
+ mockIdentityService.getName.returns(admin);
+ mockIdentityManager.getIdentity.rejects(new Error('such error'));
+ promise = promise.then(() => {
+ return context.loadCurrentParticipant()
+ .should.eventually.be.null;
+ });
+ });
+ return promise;
+ });
+
+ it('should ignore any errors from validating the identity for admin users', () => {
+ let promise = Promise.resolve();
+ ['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => {
+ mockIdentityService.getName.returns(admin);
+ mockIdentityManager.getIdentity.resolves(mockIdentity);
+ mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(new Error('such error'));
+ promise = promise.then(() => {
+ return context.loadCurrentParticipant()
+ .should.eventually.be.null;
+ });
+ });
+ return promise;
+ });
+
+ it('should ignore any errors from looking up the participant for admin users', () => {
+ let promise = Promise.resolve();
+ ['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => {
+ mockIdentityService.getName.returns(admin);
+ mockIdentityManager.getIdentity.resolves(mockIdentity);
+ mockIdentityManager.getParticipant.withArgs(mockIdentity).rejects(new Error('such error'));
+ promise = promise.then(() => {
+ return context.loadCurrentParticipant()
+ .should.eventually.be.null;
+ });
+ });
+ return promise;
});
});
@@ -427,7 +508,7 @@ describe('Context', () => {
describe('#initialize', () => {
- let mockBusinessNetworkDefinition, mockCompiledScriptBundle, mockCompiledQueryBundle, mockSystemRegistries, mockSystemIdentities, mockCompiledAclBundle;
+ let mockBusinessNetworkDefinition, mockCompiledScriptBundle, mockCompiledQueryBundle, mockSystemRegistries, mockCompiledAclBundle;
beforeEach(() => {
mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition);
@@ -443,8 +524,6 @@ describe('Context', () => {
sinon.stub(context, 'getDataService').returns(mockDataService);
mockSystemRegistries = sinon.createStubInstance(DataCollection);
mockDataService.getCollection.withArgs('$sysregistries').resolves(mockSystemRegistries);
- mockSystemIdentities = sinon.createStubInstance(DataCollection);
- mockDataService.getCollection.withArgs('$sysidentities').resolves(mockSystemIdentities);
});
it('should initialize the context', () => {
@@ -466,7 +545,6 @@ describe('Context', () => {
sinon.assert.calledOnce(context.loadCurrentParticipant);
should.equal(context.participant, null);
context.sysregistries.should.equal(mockSystemRegistries);
- context.sysidentities.should.equal(mockSystemIdentities);
});
});
@@ -498,11 +576,33 @@ describe('Context', () => {
});
});
- it('should initialize the context with a specified system identities collection', () => {
- let mockSystemIdentities2 = sinon.createStubInstance(DataCollection);
- return context.initialize({ sysidentities: mockSystemIdentities2 })
+ it('should initialize the context with the specified function name', () => {
+ return context.initialize({ function: 'suchfunc' })
+ .then(() => {
+ context.function.should.equal('suchfunc');
+ });
+ });
+
+ it('should initialize the context with the original function name if no function name specified', () => {
+ context.function = 'suchfunc';
+ return context.initialize({})
+ .then(() => {
+ context.function.should.equal('suchfunc');
+ });
+ });
+
+ it('should initialize the context with the specified arguments', () => {
+ return context.initialize({ arguments: ['sucharg1', 'sucharg2'] })
+ .then(() => {
+ context.arguments.should.deep.equal(['sucharg1', 'sucharg2']);
+ });
+ });
+
+ it('should initialize the context with the original function name if no function name specified', () => {
+ context.arguments = ['sucharg1', 'sucharg2'];
+ return context.initialize({})
.then(() => {
- context.sysidentities.should.equal(mockSystemIdentities2);
+ context.arguments.should.deep.equal(['sucharg1', 'sucharg2']);
});
});
@@ -761,12 +861,12 @@ describe('Context', () => {
describe('#getIdentityManager', () => {
it('should return a new identity manager', () => {
- let mockDataService = sinon.createStubInstance(DataService);
- sinon.stub(context, 'getDataService').returns(mockDataService);
+ let mockIdentityService = sinon.createStubInstance(IdentityService);
+ sinon.stub(context, 'getIdentityService').returns(mockIdentityService);
let mockRegistryManager = sinon.createStubInstance(RegistryManager);
sinon.stub(context, 'getRegistryManager').returns(mockRegistryManager);
- let mockSystemIdentities = sinon.createStubInstance(DataCollection);
- sinon.stub(context, 'getSystemIdentities').returns(mockSystemIdentities);
+ let mockFactory = sinon.createStubInstance(Factory);
+ sinon.stub(context, 'getFactory').returns(mockFactory);
context.getIdentityManager().should.be.an.instanceOf(IdentityManager);
});
@@ -887,22 +987,6 @@ describe('Context', () => {
});
- describe('#getSystemIdentities', () => {
-
- it('should throw if not initialized', () => {
- (() => {
- context.getSystemIdentities();
- }).should.throw(/must call initialize before calling this function/);
- });
-
- it('should return the system identities data collection', () => {
- let mockSystemIdentities = sinon.createStubInstance(DataCollection);
- context.sysidentities = mockSystemIdentities;
- context.getSystemIdentities().should.equal(mockSystemIdentities);
- });
-
- });
-
describe('#getEventNumber', () => {
it('should get the current event number', () => {
context.getEventNumber().should.equal(0);
diff --git a/packages/composer-runtime/test/engine.identities.js b/packages/composer-runtime/test/engine.identities.js
index 7fb424a291..e4bbdea38a 100644
--- a/packages/composer-runtime/test/engine.identities.js
+++ b/packages/composer-runtime/test/engine.identities.js
@@ -50,37 +50,73 @@ describe('EngineIdentities', () => {
engine = new Engine(mockContainer);
});
- describe('#addParticipantIdentity', () => {
+ describe('#issueIdentity', () => {
it('should throw for invalid arguments', () => {
- let result = engine.invoke(mockContext, 'addParticipantIdentity', ['no', 'args', 'supported', 'here']);
- return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "addParticipantIdentity", expecting "\["participantId","userId"]"/);
+ let result = engine.invoke(mockContext, 'issueIdentity', ['no', 'args', 'supported', 'here']);
+ return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "issueIdentity", expecting "\["participantFQI","identityName"]"/);
});
- it('should add the identity mapping', () => {
- mockIdentityManager.addIdentityMapping.withArgs('org.doge.Doge#DOGE_1', 'dogeid1').resolves();
- return engine.invoke(mockContext, 'addParticipantIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1'])
+ it('should issue the identity', () => {
+ mockIdentityManager.issueIdentity.withArgs('org.doge.Doge#DOGE_1', 'dogeid1').resolves();
+ return engine.invoke(mockContext, 'issueIdentity', ['org.doge.Doge#DOGE_1', 'dogeid1'])
.then(() => {
- sinon.assert.calledOnce(mockIdentityManager.addIdentityMapping);
- sinon.assert.calledWith(mockIdentityManager.addIdentityMapping, 'org.doge.Doge#DOGE_1', 'dogeid1');
+ sinon.assert.calledOnce(mockIdentityManager.issueIdentity);
+ sinon.assert.calledWith(mockIdentityManager.issueIdentity, 'org.doge.Doge#DOGE_1', 'dogeid1');
});
});
});
- describe('#removeIdentity', () => {
+ describe('#bindIdentity', () => {
it('should throw for invalid arguments', () => {
- let result = engine.invoke(mockContext, 'removeIdentity', ['no', 'args', 'supported', 'here']);
- return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "removeIdentity", expecting "\["userId"]"/);
+ let result = engine.invoke(mockContext, 'bindIdentity', ['no', 'args', 'supported', 'here']);
+ return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "bindIdentity", expecting "\["participantFQI","certificate"]"/);
});
- it('should remove the identity mapping', () => {
- mockIdentityManager.removeIdentityMapping.withArgs('dogeid1').resolves();
- return engine.invoke(mockContext, 'removeIdentity', ['dogeid1'])
+ it('should bind the identity', () => {
+ mockIdentityManager.bindIdentity.withArgs('org.doge.Doge#DOGE_1', '===== BEGIN CERTIFICATE =====').resolves();
+ return engine.invoke(mockContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', '===== BEGIN CERTIFICATE ====='])
.then(() => {
- sinon.assert.calledOnce(mockIdentityManager.removeIdentityMapping);
- sinon.assert.calledWith(mockIdentityManager.removeIdentityMapping, 'dogeid1');
+ sinon.assert.calledOnce(mockIdentityManager.bindIdentity);
+ sinon.assert.calledWith(mockIdentityManager.bindIdentity);
+ });
+ });
+
+ });
+
+ describe('#activateIdentity', () => {
+
+ it('should throw for invalid arguments', () => {
+ let result = engine.invoke(mockContext, 'activateIdentity', ['no', 'args', 'supported', 'here']);
+ return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "activateIdentity", expecting "\[]"/);
+ });
+
+ it('should activate the identity', () => {
+ mockIdentityManager.activateIdentity.resolves();
+ return engine.invoke(mockContext, 'activateIdentity', [])
+ .then(() => {
+ sinon.assert.calledOnce(mockIdentityManager.activateIdentity);
+ sinon.assert.calledWith(mockIdentityManager.activateIdentity);
+ });
+ });
+
+ });
+
+ describe('#revokeIdentity', () => {
+
+ it('should throw for invalid arguments', () => {
+ let result = engine.invoke(mockContext, 'revokeIdentity', ['no', 'args', 'supported', 'here']);
+ return result.should.be.rejectedWith(/Invalid arguments "\["no","args","supported","here"\]" to function "revokeIdentity", expecting "\["identityId"]"/);
+ });
+
+ it('should revoke the identity', () => {
+ mockIdentityManager.revokeIdentity.withArgs('dogeid1').resolves();
+ return engine.invoke(mockContext, 'revokeIdentity', ['dogeid1'])
+ .then(() => {
+ sinon.assert.calledOnce(mockIdentityManager.revokeIdentity);
+ sinon.assert.calledWith(mockIdentityManager.revokeIdentity, 'dogeid1');
});
});
diff --git a/packages/composer-runtime/test/engine.js b/packages/composer-runtime/test/engine.js
index 0f255a295d..5fc5481141 100644
--- a/packages/composer-runtime/test/engine.js
+++ b/packages/composer-runtime/test/engine.js
@@ -135,7 +135,6 @@ describe('Engine', () => {
it('should enable logging if logging specified on the init', () => {
let sysdata = sinon.createStubInstance(DataCollection);
let sysregistries = sinon.createStubInstance(DataCollection);
- let sysidentities = sinon.createStubInstance(DataCollection);
mockDataService.ensureCollection.withArgs('$sysdata').resolves(sysdata);
let mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition);
let mockScriptManager = sinon.createStubInstance(ScriptManager);
@@ -155,7 +154,6 @@ describe('Engine', () => {
mockContext.getAclCompiler.returns(mockAclCompiler);
sysdata.add.withArgs('businessnetwork', sinon.match.any).resolves();
mockDataService.ensureCollection.withArgs('$sysregistries').resolves(sysregistries);
- mockDataService.ensureCollection.withArgs('$sysidentities').resolves(sysidentities);
mockRegistryManager.ensure.withArgs('Transaction', 'default', 'Default Transaction Registry').resolves();
sandbox.stub(Context, 'cacheBusinessNetwork');
sandbox.stub(Context, 'cacheCompiledScriptBundle');
@@ -168,7 +166,7 @@ describe('Engine', () => {
sinon.assert.calledOnce(mockLoggingService.setLogLevel);
sinon.assert.calledWith(mockLoggingService.setLogLevel, 'DEBUG');
- sinon.assert.calledThrice(mockDataService.ensureCollection);
+ sinon.assert.calledTwice(mockDataService.ensureCollection);
sinon.assert.calledWith(mockDataService.ensureCollection, '$sysdata');
sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive);
sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, sinon.match((archive) => {
@@ -187,18 +185,18 @@ describe('Engine', () => {
sinon.assert.calledOnce(Context.cacheCompiledAclBundle);
sinon.assert.calledWith(Context.cacheCompiledAclBundle, 'dc9c1c09907c36f5379d615ae61c02b46ba254d92edb77cb63bdcc5247ccd01c', mockCompiledAclBundle);
sinon.assert.calledWith(mockDataService.ensureCollection, '$sysregistries');
- sinon.assert.calledWith(mockDataService.ensureCollection, '$sysidentities');
sinon.assert.calledOnce(mockRegistryManager.ensure);
sinon.assert.calledWith(mockRegistryManager.ensure, 'Transaction', 'default', 'Default Transaction Registry');
sinon.assert.calledOnce(mockRegistryManager.createDefaults);
sinon.assert.calledOnce(mockContext.initialize);
sinon.assert.calledWith(mockContext.initialize, {
+ function: 'init',
+ arguments: ['aGVsbG8gd29ybGQ=','{"logLevel": "DEBUG"}'],
businessNetworkDefinition: mockBusinessNetworkDefinition,
compiledScriptBundle: mockCompiledScriptBundle,
compiledQueryBundle: mockCompiledQueryBundle,
compiledAclBundle: mockCompiledAclBundle,
- sysregistries: sysregistries,
- sysidentities: sysidentities
+ sysregistries: sysregistries
});
sinon.assert.calledOnce(mockContext.transactionStart);
sinon.assert.calledWith(mockContext.transactionStart, false);
@@ -213,7 +211,6 @@ describe('Engine', () => {
it('should create system collections and default registries', () => {
let sysdata = sinon.createStubInstance(DataCollection);
let sysregistries = sinon.createStubInstance(DataCollection);
- let sysidentities = sinon.createStubInstance(DataCollection);
mockDataService.ensureCollection.withArgs('$sysdata').resolves(sysdata);
let mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition);
let mockScriptManager = sinon.createStubInstance(ScriptManager);
@@ -233,7 +230,6 @@ describe('Engine', () => {
mockContext.getAclCompiler.returns(mockAclCompiler);
sysdata.add.withArgs('businessnetwork', sinon.match.any).resolves();
mockDataService.ensureCollection.withArgs('$sysregistries').resolves(sysregistries);
- mockDataService.ensureCollection.withArgs('$sysidentities').resolves(sysidentities);
mockRegistryManager.ensure.withArgs('Transaction', 'default', 'Default Transaction Registry').resolves();
sandbox.stub(Context, 'cacheBusinessNetwork');
sandbox.stub(Context, 'cacheCompiledScriptBundle');
@@ -241,7 +237,7 @@ describe('Engine', () => {
return engine.init(mockContext, 'init', ['aGVsbG8gd29ybGQ=','{}'])
.then(() => {
sinon.assert.notCalled(mockLoggingService.setLogLevel);
- sinon.assert.calledThrice(mockDataService.ensureCollection);
+ sinon.assert.calledTwice(mockDataService.ensureCollection);
sinon.assert.calledWith(mockDataService.ensureCollection, '$sysdata');
sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive);
sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, sinon.match((archive) => {
@@ -256,18 +252,18 @@ describe('Engine', () => {
sinon.assert.calledOnce(Context.cacheCompiledScriptBundle);
sinon.assert.calledWith(Context.cacheCompiledScriptBundle, 'dc9c1c09907c36f5379d615ae61c02b46ba254d92edb77cb63bdcc5247ccd01c', mockCompiledScriptBundle);
sinon.assert.calledWith(mockDataService.ensureCollection, '$sysregistries');
- sinon.assert.calledWith(mockDataService.ensureCollection, '$sysidentities');
sinon.assert.calledOnce(mockRegistryManager.ensure);
sinon.assert.calledWith(mockRegistryManager.ensure, 'Transaction', 'default', 'Default Transaction Registry');
sinon.assert.calledOnce(mockRegistryManager.createDefaults);
sinon.assert.calledOnce(mockContext.initialize);
sinon.assert.calledWith(mockContext.initialize, {
+ function: 'init',
+ arguments: ['aGVsbG8gd29ybGQ=','{}'],
businessNetworkDefinition: mockBusinessNetworkDefinition,
compiledScriptBundle: mockCompiledScriptBundle,
compiledQueryBundle: mockCompiledQueryBundle,
compiledAclBundle: mockCompiledAclBundle,
- sysregistries: sysregistries,
- sysidentities: sysidentities
+ sysregistries: sysregistries
});
sinon.assert.calledOnce(mockContext.transactionStart);
sinon.assert.calledWith(mockContext.transactionStart, false);
@@ -349,6 +345,10 @@ describe('Engine', () => {
return engine.invoke(mockContext, 'test', [])
.then(() => {
sinon.assert.calledOnce(mockContext.initialize);
+ sinon.assert.calledWith(mockContext.initialize, {
+ function: 'test',
+ arguments: []
+ });
sinon.assert.calledOnce(engine.test);
sinon.assert.calledWith(engine.test, mockContext, []);
sinon.assert.calledOnce(mockContext.transactionStart);
@@ -421,6 +421,10 @@ describe('Engine', () => {
engine.test = sinon.stub().resolves({});
return engine.query(mockContext, 'test', [])
.then(() => {
+ sinon.assert.calledWith(mockContext.initialize, {
+ function: 'test',
+ arguments: []
+ });
sinon.assert.calledOnce(mockContext.initialize);
sinon.assert.calledOnce(engine.test);
sinon.assert.calledWith(engine.test, mockContext, []);
diff --git a/packages/composer-runtime/test/identitymanager.js b/packages/composer-runtime/test/identitymanager.js
index 52201d64ed..1f0f7ce18a 100644
--- a/packages/composer-runtime/test/identitymanager.js
+++ b/packages/composer-runtime/test/identitymanager.js
@@ -14,12 +14,13 @@
'use strict';
-const DataCollection = require('../lib/datacollection');
-const DataService = require('../lib/dataservice');
+const Context = require('../lib/context');
+const Factory = require('composer-common').Factory;
const IdentityManager = require('../lib/identitymanager');
+const IdentityService = require('../lib/identityservice');
+const ModelManager = require('composer-common').ModelManager;
const Registry = require('../lib/registry');
const RegistryManager = require('../lib/registrymanager');
-const Resource = require('composer-common').Resource;
const chai = require('chai');
chai.should();
@@ -31,153 +32,378 @@ require('sinon-as-promised');
describe('IdentityManager', () => {
- let mockDataService;
- let mockSystemIdentities;
+ let mockContext;
+ let mockIdentityService;
let mockRegistryManager;
- let mockRegistry;
+ let mockIdentityRegistry;
+ let modelManager;
+ let factory;
let identityManager;
- let mockParticipant;
beforeEach(() => {
- mockDataService = sinon.createStubInstance(DataService);
- mockSystemIdentities = sinon.createStubInstance(DataCollection);
- mockDataService.getCollection.withArgs('$sysidentities').resolves(mockSystemIdentities);
+ mockContext = sinon.createStubInstance(Context);
+ mockIdentityService = sinon.createStubInstance(IdentityService);
+ mockContext.getIdentityService.returns(mockIdentityService);
mockRegistryManager = sinon.createStubInstance(RegistryManager);
- mockRegistry = sinon.createStubInstance(Registry);
- mockRegistryManager.get.withArgs('Participant', 'org.doge.Doge').resolves(mockRegistry);
- identityManager = new IdentityManager(mockDataService, mockRegistryManager, mockSystemIdentities);
- mockParticipant = sinon.createStubInstance(Resource);
- mockParticipant.getIdentifier.returns('DOGE_1');
- mockParticipant.getType.returns('Doge');
- mockParticipant.getNamespace.returns('org.doge');
- mockParticipant.getFullyQualifiedType.returns('org.doge.Doge');
- mockParticipant.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1');
+ mockContext.getRegistryManager.returns(mockRegistryManager);
+ mockIdentityRegistry = sinon.createStubInstance(Registry);
+ mockRegistryManager.get.withArgs('Asset', 'org.hyperledger.composer.system.Identity').resolves(mockIdentityRegistry);
+ modelManager = new ModelManager();
+ modelManager.addModelFile(`
+ namespace org.acme
+ participant SampleParticipant identified by participantId {
+ o String participantId
+ }
+ `);
+ factory = new Factory(modelManager);
+ mockContext.getFactory.returns(factory);
+ identityManager = new IdentityManager(mockContext);
});
- describe('#addIdentityMapping', () => {
+ describe('#getIdentityRegistry', () => {
- it('should add a new mapping for a user ID to a participant specified by a resource', () => {
- // The participant exists.
- mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant);
- // An existing mapping for this user ID does not exist.
- mockSystemIdentities.exists.withArgs('dogeid1').resolves(false);
- return identityManager.addIdentityMapping(mockParticipant, 'dogeid1')
+ it('should get the identity registry', () => {
+ return identityManager.getIdentityRegistry()
+ .should.eventually.be.equal(mockIdentityRegistry);
+ });
+
+ });
+
+ describe('#getIdentity', () => {
+
+ it('should look up an identity using the identifier', () => {
+ mockIdentityService.getIdentifier.returns('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63');
+ let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', '7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63');
+ mockIdentityRegistry.exists.withArgs('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63').resolves(true);
+ mockIdentityRegistry.get.withArgs('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63').resolves(identity);
+ return identityManager.getIdentity()
+ .should.eventually.be.equal(identity);
+ });
+
+ it('should look up an issued identity using the name and issuer', () => {
+ mockIdentityService.getIdentifier.returns('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63');
+ mockIdentityRegistry.exists.withArgs('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63').resolves(false);
+ mockIdentityService.getName.returns('admin');
+ mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417');
+ let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', '9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50');
+ mockIdentityRegistry.exists.withArgs('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50').resolves(true);
+ mockIdentityRegistry.get.withArgs('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50').resolves(identity);
+ return identityManager.getIdentity()
+ .should.eventually.be.equal(identity);
+ });
+
+ it('should throw for an identity that does not exist', () => {
+ mockIdentityService.getIdentifier.returns('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63');
+ mockIdentityRegistry.exists.withArgs('7d85a9672abea0dfa45705eed99a536b8470d192e2a17e50c1fb86cb4bccae63').resolves(false);
+ mockIdentityService.getName.returns('admin');
+ mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417');
+ mockIdentityRegistry.exists.withArgs('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50').resolves(false);
+ return identityManager.getIdentity()
+ .should.be.rejectedWith(/The current identity has not been registered/);
+ });
+
+ });
+
+ describe('#validateIdentity', () => {
+
+ let identity;
+
+ beforeEach(() => {
+ identity = factory.newResource('org.hyperledger.composer.system', 'Identity', '9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50');
+ });
+
+ it('should throw for a revoked identity', () => {
+ identity.state = 'REVOKED';
+ (() => {
+ identityManager.validateIdentity(identity);
+ }).should.throw(/The current identity has been revoked/);
+ });
+
+ it('should throw for an issued identity that requires activation', () => {
+ identity.state = 'ISSUED';
+ (() => {
+ identityManager.validateIdentity(identity);
+ }).should.throw(/The current identity must be activated \(ACTIVATION_REQUIRED\)/);
+ });
+
+ it('should throw for a bound identity that requires activation', () => {
+ identity.state = 'BOUND';
+ (() => {
+ identityManager.validateIdentity(identity);
+ }).should.throw(/The current identity must be activated \(ACTIVATION_REQUIRED\)/);
+ });
+
+ it('should throw for an identity in an unknown state', () => {
+ identity.state = 'WOOPWOOP';
+ (() => {
+ identityManager.validateIdentity(identity);
+ }).should.throw(/The current identity is in an unknown state/);
+ });
+
+ it('should not throw for an activated identity', () => {
+ identity.state = 'ACTIVATED';
+ identityManager.validateIdentity(identity);
+ });
+
+ });
+
+ describe('#getParticipant', () => {
+
+ let identity;
+ let participant;
+ let mockParticipantRegistry;
+
+ beforeEach(() => {
+ identity = factory.newResource('org.hyperledger.composer.system', 'Identity', '9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50');
+ participant = factory.newResource('org.acme', 'SampleParticipant', 'alice@email.com');
+ identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com');
+ mockParticipantRegistry = sinon.createStubInstance(Registry);
+ mockRegistryManager.get.withArgs('Participant', 'org.acme.SampleParticipant').resolves(mockParticipantRegistry);
+ });
+
+ it('should get the participant for the identity', () => {
+ mockParticipantRegistry.get.withArgs('alice@email.com').resolves(participant);
+ return identityManager.getParticipant(identity)
+ .should.eventually.be.equal(participant);
+ });
+
+ it('should throw if the participant does not exist', () => {
+ mockParticipantRegistry.get.withArgs('alice@email.com').rejects(new Error('such error'));
+ return identityManager.getParticipant(identity)
+ .should.be.rejectedWith(/The current identity is bound to a participant that does not exist/);
+ });
+
+ });
+
+ describe('#parseParticipant', () => {
+
+ it('should throw for a missing hash', () => {
+ (() => {
+ identityManager.parseParticipant('org.acme.SampleParticipant_alice@email.com');
+ }).should.throw(/Invalid fully qualified participant identifier/);
+ });
+
+ it('should parse the participant FQI into a relationship', () => {
+ const relationship = identityManager.parseParticipant('org.acme.SampleParticipant#alice@email.com');
+ relationship.getNamespace().should.equal('org.acme');
+ relationship.getType().should.equal('SampleParticipant');
+ relationship.getIdentifier().should.equal('alice@email.com');
+ });
+
+ });
+
+ describe('#issueIdentity', () => {
+
+ it('should add a new identity to the identity registry', () => {
+ mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417');
+ return identityManager.issueIdentity('org.acme.SampleParticipant#alice@email.com', 'alice1')
.then(() => {
- sinon.assert.calledOnce(mockSystemIdentities.add);
- sinon.assert.calledWith(mockSystemIdentities.add, 'dogeid1', {
- participant: 'org.doge.Doge#DOGE_1'
- });
+ sinon.assert.calledOnce(mockIdentityRegistry.add);
+ const identity = mockIdentityRegistry.add.args[0][0];
+ identity.getIdentifier().should.equal('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f');
+ identity.name.should.equal('alice1');
+ identity.issuer.should.equal('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417');
+ identity.certificate.should.equal('');
+ identity.state.should.equal('ISSUED');
+ identity.participant.getNamespace().should.equal('org.acme');
+ identity.participant.getType().should.equal('SampleParticipant');
+ identity.participant.getIdentifier().should.equal('alice@email.com');
});
});
- it('should add a new mapping for a user ID to a participant specified by an identifier', () => {
- // The participant exists.
- mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant);
- // An existing mapping for this user ID does not exist.
- mockSystemIdentities.exists.withArgs('dogeid1').resolves(false);
- return identityManager.addIdentityMapping('org.doge.Doge#DOGE_1', 'dogeid1')
+ });
+
+ describe('#bindIdentity', () => {
+
+ const pem = '-----BEGIN CERTIFICATE-----\nMIICGjCCAcCgAwIBAgIRANuOnVN+yd/BGyoX7ioEklQwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNjI2MTI0OTI2WhcNMjcwNjI0MTI0OTI2\nWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWQWRtaW5Ab3JnMS5leGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGu8KxBQ1GkxSTMVoLv7NXiYKWj5t6Dh\nWRTJBHnLkWV7lRUfYaKAKFadSii5M7Z7ZpwD8NS7IsMdPR6Z4EyGgwKjTTBLMA4G\nA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIBmrZau7BIB9\nrRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0gAMEUCIQC4sKQ6\nCEgqbTYe48az95W9/hnZ+7DI5eSnWUwV9vCd/gIgS5K6omNJydoFoEpaEIwM97uS\nXVMHPa0iyC497vdNURA=\n-----END CERTIFICATE-----\n';
+
+ it('should add a new identity to the identity registry', () => {
+ mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417');
+ return identityManager.bindIdentity('org.acme.SampleParticipant#alice@email.com', pem)
.then(() => {
- sinon.assert.calledOnce(mockSystemIdentities.add);
- sinon.assert.calledWith(mockSystemIdentities.add, 'dogeid1', {
- participant: 'org.doge.Doge#DOGE_1'
- });
+ sinon.assert.calledOnce(mockIdentityRegistry.add);
+ const identity = mockIdentityRegistry.add.args[0][0];
+ identity.getIdentifier().should.equal('2be26f6d4757b49fbae46f9fbb0c225b2f77508a882e4dd899700b56f62ad639');
+ identity.name.should.equal('');
+ identity.issuer.should.equal('');
+ identity.certificate.should.equal(pem);
+ identity.state.should.equal('BOUND');
+ identity.participant.getNamespace().should.equal('org.acme');
+ identity.participant.getType().should.equal('SampleParticipant');
+ identity.participant.getIdentifier().should.equal('alice@email.com');
});
});
- it('should throw if the specified participant does not exist', () => {
- // The participant does not exist.
- mockRegistry.get.withArgs('DOGE_1').rejects(new Error('does not exist'));
- // An existing mapping for this user ID does not exist.
- mockSystemIdentities.exists.withArgs('dogeid1').resolves(false);
- return identityManager.addIdentityMapping('org.doge.Doge#DOGE_1', 'dogeid1')
- .should.be.rejectedWith(/does not exist/);
+ });
+
+ describe('#activateIdentity', () => {
+
+ const pem = '-----BEGIN CERTIFICATE-----\nMIICGjCCAcCgAwIBAgIRANuOnVN+yd/BGyoX7ioEklQwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNjI2MTI0OTI2WhcNMjcwNjI0MTI0OTI2\nWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWQWRtaW5Ab3JnMS5leGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGu8KxBQ1GkxSTMVoLv7NXiYKWj5t6Dh\nWRTJBHnLkWV7lRUfYaKAKFadSii5M7Z7ZpwD8NS7IsMdPR6Z4EyGgwKjTTBLMA4G\nA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIBmrZau7BIB9\nrRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0gAMEUCIQC4sKQ6\nCEgqbTYe48az95W9/hnZ+7DI5eSnWUwV9vCd/gIgS5K6omNJydoFoEpaEIwM97uS\nXVMHPa0iyC497vdNURA=\n-----END CERTIFICATE-----\n';
+
+ it('should activate an issued identity', () => {
+ let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f');
+ identity.name = 'alice1';
+ identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417';
+ identity.certificate = '';
+ identity.state = 'ISSUED';
+ identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com');
+ sinon.stub(identityManager, 'getIdentity').resolves(identity);
+ sinon.stub(identityManager, 'activateIssuedIdentity').resolves();
+ return identityManager.activateIdentity()
+ .then(() => {
+ sinon.assert.calledOnce(identityManager.activateIssuedIdentity);
+ sinon.assert.calledWith(identityManager.activateIssuedIdentity, mockIdentityRegistry, identity);
+ });
});
- it('should throw if the specified participant ID is invalid', () => {
- // The participant does not exist.
- mockRegistry.get.withArgs('DOGE_1').rejects(new Error('does not exist'));
- // An existing mapping for this user ID does not exist.
- mockSystemIdentities.exists.withArgs('dogeid1').resolves(false);
- (() => {
- identityManager.addIdentityMapping('org.doge.Doge$DOGE_1', 'dogeid1');
- }).should.throw(/Invalid fully qualified participant identifier/);
+ it('should activate a bound identity', () => {
+ let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f');
+ identity.name = 'alice1';
+ identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417';
+ identity.certificate = pem;
+ identity.state = 'BOUND';
+ identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com');
+ sinon.stub(identityManager, 'getIdentity').resolves(identity);
+ sinon.stub(identityManager, 'activateBoundIdentity').resolves();
+ return identityManager.activateIdentity()
+ .then(() => {
+ sinon.assert.calledOnce(identityManager.activateBoundIdentity);
+ sinon.assert.calledWith(identityManager.activateBoundIdentity, mockIdentityRegistry, identity);
+ });
});
- it('should throw if the specified user ID is already mapped', () => {
- // The participant exists.
- mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant);
- // An existing mapping for this user ID does exist.
- mockSystemIdentities.exists.withArgs('dogeid1').resolves(true);
- return identityManager.addIdentityMapping('org.doge.Doge#DOGE_1', 'dogeid1')
- .should.be.rejectedWith(/Found an existing mapping for user ID/);
+ it('should throw for an already activated identity', () => {
+ let identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f');
+ identity.name = 'alice1';
+ identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417';
+ identity.certificate = pem;
+ identity.state = 'ACTIVATED';
+ identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com');
+ sinon.stub(identityManager, 'getIdentity').resolves(identity);
+ return identityManager.activateIdentity()
+ .should.be.rejectedWith(/The current identity cannot be activated because it is in an unknown state/);
});
});
- describe('#removeIdentityMapping', () => {
+ describe('#activateIssuedIdentity', () => {
+
+ const pem = '-----BEGIN CERTIFICATE-----\nMIICGjCCAcCgAwIBAgIRANuOnVN+yd/BGyoX7ioEklQwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNjI2MTI0OTI2WhcNMjcwNjI0MTI0OTI2\nWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWQWRtaW5Ab3JnMS5leGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGu8KxBQ1GkxSTMVoLv7NXiYKWj5t6Dh\nWRTJBHnLkWV7lRUfYaKAKFadSii5M7Z7ZpwD8NS7IsMdPR6Z4EyGgwKjTTBLMA4G\nA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIBmrZau7BIB9\nrRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0gAMEUCIQC4sKQ6\nCEgqbTYe48az95W9/hnZ+7DI5eSnWUwV9vCd/gIgS5K6omNJydoFoEpaEIwM97uS\nXVMHPa0iyC497vdNURA=\n-----END CERTIFICATE-----\n';
+
+ let identity;
- it('should remove an existing mapping for a user ID to a participant', () => {
- // An existing mapping for this user ID does exist.
- mockSystemIdentities.exists.withArgs('dogeid1').resolves(true);
- return identityManager.removeIdentityMapping('dogeid1')
+ beforeEach(() => {
+ identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f');
+ identity.name = 'alice1';
+ identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417';
+ identity.certificate = '';
+ identity.state = 'ISSUED';
+ identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com');
+ });
+
+ it('should throw for an invalid issuer', () => {
+ mockIdentityService.getIdentifier.returns('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50');
+ mockIdentityService.getName.returns('alice1');
+ mockIdentityService.getIssuer.returns('f15148476b739e6329781a5b963cc30dc583a124408da8222418a7553843c251');
+ mockIdentityService.getCertificate.returns(pem);
+ mockIdentityRegistry.remove.resolves();
+ mockIdentityRegistry.add.resolves();
+ (() => {
+ identityManager.activateIssuedIdentity(mockIdentityRegistry, identity);
+ }).should.throw(/The current identity cannot be activated because the issuer is invalid/);
+ });
+
+ it('should remove and add the updated identity in the identity registry', () => {
+ mockIdentityService.getIdentifier.returns('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50');
+ mockIdentityService.getName.returns('alice1');
+ mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417');
+ mockIdentityService.getCertificate.returns(pem);
+ mockIdentityRegistry.remove.resolves();
+ mockIdentityRegistry.add.resolves();
+ return identityManager.activateIssuedIdentity(mockIdentityRegistry, identity)
.then(() => {
- sinon.assert.calledOnce(mockSystemIdentities.remove);
- sinon.assert.calledWith(mockSystemIdentities.remove, 'dogeid1');
+ sinon.assert.calledOnce(mockIdentityRegistry.remove);
+ sinon.assert.calledWith(mockIdentityRegistry.remove, identity);
+ sinon.assert.calledOnce(mockIdentityRegistry.add);
+ const newIdentity = mockIdentityRegistry.add.args[0][0];
+ newIdentity.getIdentifier().should.equal('9f8b1a1e7280d40f4f14577ee4329473c60f04db3ea0f40c91f9684558c6ab50');
+ newIdentity.name.should.equal('alice1');
+ newIdentity.issuer.should.equal('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417');
+ newIdentity.certificate.should.equal(pem);
+ newIdentity.state.should.equal('ACTIVATED');
+ newIdentity.participant.getNamespace().should.equal('org.acme');
+ newIdentity.participant.getType().should.equal('SampleParticipant');
+ newIdentity.participant.getIdentifier().should.equal('alice@email.com');
});
});
- it('should not throw if an existing mapping for a user ID does not exist', () => {
- // An existing mapping for this user ID does not exist.
- mockSystemIdentities.exists.withArgs('dogeid1').resolves(false);
- return identityManager.removeIdentityMapping('dogeid1');
+ });
+
+ describe('#activateBoundIdentity', () => {
+
+ const pem = '-----BEGIN CERTIFICATE-----\nMIICGjCCAcCgAwIBAgIRANuOnVN+yd/BGyoX7ioEklQwCgYIKoZIzj0EAwIwczEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh\nLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNjI2MTI0OTI2WhcNMjcwNjI0MTI0OTI2\nWjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nU2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWQWRtaW5Ab3JnMS5leGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGu8KxBQ1GkxSTMVoLv7NXiYKWj5t6Dh\nWRTJBHnLkWV7lRUfYaKAKFadSii5M7Z7ZpwD8NS7IsMdPR6Z4EyGgwKjTTBLMA4G\nA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIBmrZau7BIB9\nrRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0gAMEUCIQC4sKQ6\nCEgqbTYe48az95W9/hnZ+7DI5eSnWUwV9vCd/gIgS5K6omNJydoFoEpaEIwM97uS\nXVMHPa0iyC497vdNURA=\n-----END CERTIFICATE-----\n';
+
+ let identity;
+
+ beforeEach(() => {
+ identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f');
+ identity.name = 'alice1';
+ identity.issuer = '26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417';
+ identity.certificate = pem;
+ identity.state = 'ISSUED';
+ identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com');
+ });
+
+ it('should update the identity in the identity registry', () => {
+ mockIdentityService.getName.returns('alice1');
+ mockIdentityService.getIssuer.returns('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417');
+ mockIdentityRegistry.update.resolves();
+ return identityManager.activateBoundIdentity(mockIdentityRegistry, identity)
+ .then(() => {
+ sinon.assert.calledOnce(mockIdentityRegistry.update);
+ const newIdentity = mockIdentityRegistry.update.args[0][0];
+ newIdentity.getIdentifier().should.equal('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f');
+ newIdentity.name.should.equal('alice1');
+ newIdentity.issuer.should.equal('26341f1fc63f30886c54abeba3aca520601126ae2a57869d1ec1a3f854ebc417');
+ newIdentity.certificate.should.equal(pem);
+ newIdentity.state.should.equal('ACTIVATED');
+ newIdentity.participant.getNamespace().should.equal('org.acme');
+ newIdentity.participant.getType().should.equal('SampleParticipant');
+ newIdentity.participant.getIdentifier().should.equal('alice@email.com');
+ });
});
});
- describe('#getParticipant', () => {
+ describe('#revokeIdentity', () => {
- it('should resolve to a participant for an existing mapping and participant', () => {
- // An existing mapping for this user ID does exist.
- mockSystemIdentities.get.withArgs('dogeid1').resolves({
- participant: 'org.doge.Doge#DOGE_1'
- });
- // The participant exists.
- mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant);
- return identityManager.getParticipant('dogeid1')
- .then((participant) => {
- participant.should.equal(mockParticipant);
- });
+ let identity;
+
+ beforeEach(() => {
+ identity = factory.newResource('org.hyperledger.composer.system', 'Identity', 'e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f');
+ identity.name = '';
+ identity.issuer = '';
+ identity.certificate = '';
+ identity.state = 'ISSUED';
+ identity.participant = factory.newRelationship('org.acme', 'SampleParticipant', 'alice@email.com');
+ mockIdentityRegistry.get.withArgs('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f').resolves(identity);
});
- it('should throw an error for a missing mapping', () => {
- // An existing mapping for this user ID does not exist.
- mockSystemIdentities.get.withArgs('dogeid1').rejects(new Error('no such mapping'));
- // The participant exists.
- mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant);
- return identityManager.getParticipant('dogeid1')
- .should.be.rejectedWith(/no such mapping/);
- });
-
- it('should throw an error for an invalid mapping', () => {
- // An existing mapping for this user ID does not exist.
- mockSystemIdentities.get.withArgs('dogeid1').resolves({
- participant: 'org.doge.Doge@DOGE_1'
- });
- // The participant exists.
- mockRegistry.get.withArgs('DOGE_1').resolves(mockParticipant);
- return identityManager.getParticipant('dogeid1')
- .should.be.rejectedWith(/Invalid fully qualified participant identifier/);
- });
-
- it('should throw an error for an existing mapping but missing participant', () => {
- // An existing mapping for this user ID does exist.
- mockSystemIdentities.get.withArgs('dogeid1').resolves({
- participant: 'org.doge.Doge#DOGE_1'
- });
- // The participant does not exist.
- mockRegistry.get.withArgs('DOGE_1').rejects(new Error('no such participant'));
- return identityManager.getParticipant('dogeid1')
- .should.be.rejectedWith(/no such participant/);
+ it('should update the identity in the identity registry', () => {
+ mockIdentityRegistry.update.resolves();
+ return identityManager.revokeIdentity('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f')
+ .then(() => {
+ sinon.assert.calledOnce(mockIdentityRegistry.update);
+ const newIdentity = mockIdentityRegistry.update.args[0][0];
+ newIdentity.getIdentifier().should.equal('e38b9db5b7167f8033fb48f04efe5d5fd7ec36c8d7be51ed019f81e1ac66657f');
+ newIdentity.name.should.equal('');
+ newIdentity.issuer.should.equal('');
+ newIdentity.certificate.should.equal('');
+ newIdentity.state.should.equal('REVOKED');
+ newIdentity.participant.getNamespace().should.equal('org.acme');
+ newIdentity.participant.getType().should.equal('SampleParticipant');
+ newIdentity.participant.getIdentifier().should.equal('alice@email.com');
+ });
});
});
diff --git a/packages/composer-runtime/test/identityservice.js b/packages/composer-runtime/test/identityservice.js
index 63688dad79..9dcc94e6fe 100644
--- a/packages/composer-runtime/test/identityservice.js
+++ b/packages/composer-runtime/test/identityservice.js
@@ -22,11 +22,41 @@ describe('IdentityService', () => {
let identityService = new IdentityService();
- describe('#getCurrentUserID', () => {
+ describe('#getIdentifier', () => {
it('should throw as abstract method', () => {
(() => {
- identityService.getCurrentUserID();
+ identityService.getIdentifier();
+ }).should.throw(/abstract function called/);
+ });
+
+ });
+
+ describe('#getName', () => {
+
+ it('should throw as abstract method', () => {
+ (() => {
+ identityService.getName();
+ }).should.throw(/abstract function called/);
+ });
+
+ });
+
+ describe('#getIssuer', () => {
+
+ it('should throw as abstract method', () => {
+ (() => {
+ identityService.getIssuer();
+ }).should.throw(/abstract function called/);
+ });
+
+ });
+
+ describe('#getCertificate', () => {
+
+ it('should throw as abstract method', () => {
+ (() => {
+ identityService.getCertificate();
}).should.throw(/abstract function called/);
});
diff --git a/packages/composer-systests/systest/identities.js b/packages/composer-systests/systest/identities.js
index 48ae3afe63..ba97e4f077 100644
--- a/packages/composer-systests/systest/identities.js
+++ b/packages/composer-systests/systest/identities.js
@@ -127,7 +127,7 @@ describe('Identity system tests', () => {
client = result;
return client.ping();
})
- .should.be.rejectedWith(/The identity may be invalid or may have been revoked/);
+ .should.be.rejectedWith(/The current identity is bound to a participant that does not exist/);
});
it('should issue an identity and make the participant available for transaction processor functions', () => {
@@ -180,7 +180,7 @@ describe('Identity system tests', () => {
let transaction = factory.newTransaction('systest.identities', 'SampleTransaction');
return client.submitTransaction(transaction);
})
- .should.be.rejectedWith(/The identity may be invalid or may have been revoked/);
+ .should.be.rejectedWith(/The current identity is bound to a participant that does not exist/);
});
});
From ab3d3e3f0fc55e2ac91e41bac8f16442ea9cf8ab Mon Sep 17 00:00:00 2001
From: Simon Stone
Date: Wed, 12 Jul 2017 22:25:09 +0100
Subject: [PATCH 2/7] Get identities working again on embedded and web runtimes
(#1546)
* Cannot activate issued identity as identity not found
* Get identities working again on embedded and web runtimes
---
.../lib/embeddedconnection.js | 109 +++++++++------
.../lib/embeddedsecuritycontext.js | 14 +-
.../test/embeddedconnection.js | 113 ++++++++-------
.../test/embeddedsecuritycontext.js | 20 ++-
.../lib/webconnection.js | 129 +++++++++++-------
.../lib/websecuritycontext.js | 14 +-
.../test/webconnection.js | 119 +++++++++-------
.../test/websecuritycontext.js | 20 ++-
.../lib/embeddedcontext.js | 6 +-
.../lib/embeddedidentityservice.js | 39 ++++--
.../test/embeddedcontext.js | 21 +--
.../test/embeddedidentityservice.js | 51 ++++++-
.../composer-runtime-web/lib/webcontext.js | 6 +-
.../lib/webidentityservice.js | 39 ++++--
.../composer-runtime-web/test/webcontext.js | 19 +--
.../test/webidentityservice.js | 42 +++++-
packages/composer-runtime/lib/context.js | 11 +-
packages/composer-runtime/test/context.js | 2 +-
18 files changed, 495 insertions(+), 279 deletions(-)
diff --git a/packages/composer-connector-embedded/lib/embeddedconnection.js b/packages/composer-connector-embedded/lib/embeddedconnection.js
index cc5f0f541c..ad56eb8d18 100644
--- a/packages/composer-connector-embedded/lib/embeddedconnection.js
+++ b/packages/composer-connector-embedded/lib/embeddedconnection.js
@@ -15,6 +15,7 @@
'use strict';
const Connection = require('composer-common').Connection;
+const createHash = require('sha.js');
const Engine = require('composer-runtime').Engine;
const EmbeddedContainer = require('composer-runtime-embedded').EmbeddedContainer;
const EmbeddedContext = require('composer-runtime-embedded').EmbeddedContext;
@@ -28,6 +29,9 @@ const businessNetworks = {};
// A mapping of chaincode IDs to their instance objects.
const chaincodes = {};
+// The issuer for all identities.
+const DEFAULT_ISSUER = createHash('sha256').update('org1').digest('hex');
+
/**
* Base class representing a connection to a business network.
* @protected
@@ -148,20 +152,19 @@ class EmbeddedConnection extends Connection {
* object representing the logged in participant, or rejected with a login error.
*/
login(enrollmentID, enrollmentSecret) {
- // The 'admin' ID is special for the moment as it is not bound to a participant.
- let result = new EmbeddedSecurityContext(this, enrollmentID !== 'admin' ? enrollmentID : null);
if (!this.businessNetworkIdentifier) {
return this.testIdentity(enrollmentID, enrollmentSecret)
- .then(() => {
- return result;
+ .then((identity) => {
+ return new EmbeddedSecurityContext(this, identity);
});
}
return this.testIdentity(enrollmentID, enrollmentSecret)
- .then(() => {
+ .then((identity) => {
let chaincodeUUID = EmbeddedConnection.getBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile);
if (!chaincodeUUID) {
throw new Error(`No chaincode ID found for business network '${this.businessNetworkIdentifier}'`);
}
+ const result = new EmbeddedSecurityContext(this, identity);
result.setChaincodeID(chaincodeUUID);
return result;
});
@@ -177,12 +180,12 @@ class EmbeddedConnection extends Connection {
*/
deploy(securityContext, businessNetwork, deployOptions) {
let container = EmbeddedConnection.createContainer();
- let userID = securityContext.getUserID();
+ let identity = securityContext.getIdentity();
let chaincodeUUID = container.getUUID();
let engine = EmbeddedConnection.createEngine(container);
EmbeddedConnection.addBusinessNetwork(businessNetwork.getName(), this.connectionProfile, chaincodeUUID);
EmbeddedConnection.addChaincode(chaincodeUUID, container, engine);
- let context = new EmbeddedContext(engine, userID, this);
+ let context = new EmbeddedContext(engine, identity, this);
return businessNetwork.toArchive({ date: new Date(545184000000) })
.then((businessNetworkArchive) => {
const initArgs = {};
@@ -244,10 +247,10 @@ class EmbeddedConnection extends Connection {
* chaincode function once it has been invoked, or rejected with an error.
*/
queryChainCode(securityContext, functionName, args) {
- let userID = securityContext.getUserID();
+ let identity = securityContext.getIdentity();
let chaincodeUUID = securityContext.getChaincodeID();
let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID);
- let context = new EmbeddedContext(chaincode.engine, userID, this);
+ let context = new EmbeddedContext(chaincode.engine, identity, this);
return chaincode.engine.query(context, functionName, args)
.then((data) => {
return Buffer.from(JSON.stringify(data));
@@ -263,10 +266,10 @@ class EmbeddedConnection extends Connection {
* has been invoked, or rejected with an error.
*/
invokeChainCode(securityContext, functionName, args) {
- let userID = securityContext.getUserID();
+ let identity = securityContext.getIdentity();
let chaincodeUUID = securityContext.getChaincodeID();
let chaincode = EmbeddedConnection.getChaincode(chaincodeUUID);
- let context = new EmbeddedContext(chaincode.engine, userID, this);
+ let context = new EmbeddedContext(chaincode.engine, identity, this);
return chaincode.engine.invoke(context, functionName, args)
.then((data) => {
return undefined;
@@ -275,47 +278,57 @@ class EmbeddedConnection extends Connection {
/**
* Get the data collection that stores identities.
- * @return {DataCollection} The data collection that stores identities.
+ * @return {Promise} A promise that is resolved with the data collection
+ * that stores identities.
*/
getIdentities() {
- return this.dataService.existsCollection('identities')
- .then((exists) => {
- if (exists) {
- return this.dataService.getCollection('identities');
- } else {
- return this.dataService.createCollection('identities');
- }
- });
+ return this.dataService.ensureCollection('identities');
}
/**
- * Test the specified user ID and secret to ensure that it is valid.
- * @param {string} userID The user ID.
- * @param {string} userSecret The user secret.
- * @return {Promise} A promise that is resolved if the user ID and secret
- * is valid, or rejected with an error.
+ * Get the identity for the specified name.
+ * @param {string} identityName The name for the identity.
+ * @return {Promise} A promise that is resolved with the identity, or
+ * rejected with an error.
*/
- testIdentity(userID, userSecret) {
- // The 'admin' ID is special for the moment as it is not bound to a participant.
- if (userID === 'admin') {
- return Promise.resolve();
+ getIdentity(identityName) {
+ if (identityName === 'admin') {
+ return Promise.resolve({
+ identifier: '',
+ name: 'admin',
+ issuer: DEFAULT_ISSUER,
+ secret: 'adminpw'
+ });
}
return this.getIdentities()
.then((identities) => {
- return identities.get(userID);
- })
+ return identities.get(identityName);
+ });
+ }
+
+ /**
+ * Test the specified identity name and secret to ensure that it is valid.
+ * @param {string} identityName The name for the identity.
+ * @param {string} identitySecret The secret for the identity.
+ * @return {Promise} A promise that is resolved if the user ID and secret
+ * is valid, or rejected with an error.
+ */
+ testIdentity(identityName, identitySecret) {
+ return this.getIdentity(identityName)
.then((identity) => {
- if (identity.userSecret !== userSecret) {
- throw new Error(`The user secret ${userSecret} specified for the user ID ${userID} does not match the stored user secret ${identity.userSecret}`);
+ if (identityName !== 'admin') {
+ if (identity.secret !== identitySecret) {
+ throw new Error(`The secret ${identitySecret} specified for the identity ${identityName} does not match the stored secret ${identity.secret}`);
+ }
}
+ return identity;
});
-
}
/**
- * Create a new identity for the specified user ID.
+ * Create a new identity for the specified name.
* @param {SecurityContext} securityContext The participant's security context.
- * @param {string} userID The user ID.
+ * @param {string} identityName The name for the new identity.
* @param {object} [options] Options for the new identity.
* @param {boolean} [options.issuer] Whether or not the new identity should have
* permissions to create additional new identities. False by default.
@@ -324,22 +337,32 @@ class EmbeddedConnection extends Connection {
* @return {Promise} A promise that is resolved with a generated user
* secret once the new identity has been created, or rejected with an error.
*/
- createIdentity(securityContext, userID, options) {
+ createIdentity(securityContext, identityName, options) {
let identities;
return this.getIdentities()
.then((identities_) => {
identities = identities_;
- return identities.exists(userID);
+ return identities.exists(identityName);
})
.then((exists) => {
if (exists) {
- return identities.get(userID);
+ return identities.get(identityName);
}
- const userSecret = uuid.v4().substring(0, 8);
- const identity = { userID: userID, userSecret: userSecret };
- return identities.add(userID, identity)
+ const identifier = createHash('sha256').update(uuid.v4()).digest('hex');
+ const secret = uuid.v4().substring(0, 8);
+ const identity = {
+ identifier,
+ name: identityName,
+ issuer: DEFAULT_ISSUER,
+ secret,
+ certificate: ''
+ };
+ return identities.add(identityName, identity)
.then(() => {
- return identity;
+ return {
+ userID: identity.name,
+ userSecret: identity.secret
+ };
});
});
}
diff --git a/packages/composer-connector-embedded/lib/embeddedsecuritycontext.js b/packages/composer-connector-embedded/lib/embeddedsecuritycontext.js
index f89ed9e435..671a1d7c3b 100644
--- a/packages/composer-connector-embedded/lib/embeddedsecuritycontext.js
+++ b/packages/composer-connector-embedded/lib/embeddedsecuritycontext.js
@@ -24,20 +24,20 @@ class EmbeddedSecurityContext extends SecurityContext {
/**
* Constructor.
* @param {Connection} connection The owning connection.
- * @param {String} userID The current user ID.
+ * @param {Object} identity The current identity.
*/
- constructor(connection, userID) {
+ constructor(connection, identity) {
super(connection);
- this.userID = userID;
+ this.identity = identity;
this.chaincodeID = null;
}
/**
- * Get the current user ID.
- * @return {string} The current user ID.
+ * Get the current identity.
+ * @return {string} The current identity.
*/
- getUserID() {
- return this.userID;
+ getIdentity() {
+ return this.identity;
}
/**
diff --git a/packages/composer-connector-embedded/test/embeddedconnection.js b/packages/composer-connector-embedded/test/embeddedconnection.js
index fd69228848..bd3b395791 100644
--- a/packages/composer-connector-embedded/test/embeddedconnection.js
+++ b/packages/composer-connector-embedded/test/embeddedconnection.js
@@ -35,6 +35,14 @@ require('sinon-as-promised');
describe('EmbeddedConnection', () => {
+ const identity = {
+ identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a',
+ name: 'bob1',
+ issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665',
+ secret: 'suchsecret',
+ certificate: ''
+ };
+
let sandbox;
let mockConnectionManager;
let mockSecurityContext;
@@ -94,25 +102,13 @@ describe('EmbeddedConnection', () => {
sandbox.stub(connection, 'testIdentity').resolves();
});
- it('should return a new security context with a null user ID if the admin ID is specified', () => {
- connection = new EmbeddedConnection(mockConnectionManager, 'devFabric1');
- sandbox.stub(connection, 'testIdentity').resolves();
- return connection.login('admin', 'suchs3cret')
- .then((securityContext) => {
- securityContext.should.be.an.instanceOf(EmbeddedSecurityContext);
- should.equal(securityContext.getUserID(), null);
- should.equal(securityContext.getChaincodeID(), null);
- sinon.assert.calledWith(connection.testIdentity, 'admin', 'suchs3cret');
- });
- });
-
it('should return a new security context with a null chaincode ID if the business network was not specified', () => {
connection = new EmbeddedConnection(mockConnectionManager, 'devFabric1');
- sandbox.stub(connection, 'testIdentity').resolves();
+ sandbox.stub(connection, 'testIdentity').resolves(identity);
return connection.login('doge', 'suchs3cret')
.then((securityContext) => {
securityContext.should.be.an.instanceOf(EmbeddedSecurityContext);
- securityContext.getUserID().should.equal('doge');
+ securityContext.getIdentity().should.deep.equal(identity);
should.equal(securityContext.getChaincodeID(), null);
sinon.assert.calledWith(connection.testIdentity, 'doge', 'suchs3cret');
});
@@ -143,7 +139,7 @@ describe('EmbeddedConnection', () => {
mockBusinessNetwork.getName.returns('testnetwork');
let mockContainer = sinon.createStubInstance(EmbeddedContainer);
mockContainer.getUUID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f');
- mockSecurityContext.getUserID.returns('bob1');
+ mockSecurityContext.getIdentity.returns(identity);
sandbox.stub(EmbeddedConnection, 'createContainer').returns(mockContainer);
let mockEngine = sinon.createStubInstance(Engine);
mockEngine.getContainer.returns(mockContainer);
@@ -155,7 +151,7 @@ describe('EmbeddedConnection', () => {
sinon.assert.calledOnce(mockEngine.init);
sinon.assert.calledWith(mockEngine.init, sinon.match((context) => {
context.should.be.an.instanceOf(Context);
- context.getIdentityService().getCurrentUserID().should.equal('bob1');
+ context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
return true;
}), 'init', ['aGVsbG8gd29ybGQ=', '{}']);
sinon.assert.calledOnce(connection.ping);
@@ -245,7 +241,7 @@ describe('EmbeddedConnection', () => {
mockEngine.getContainer.returns(mockContainer);
EmbeddedConnection.addBusinessNetwork('org.acme.Business', 'devFabric1', '6eeb8858-eced-4a32-b1cd-2491f1e3718f');
EmbeddedConnection.addChaincode('6eeb8858-eced-4a32-b1cd-2491f1e3718f', mockContainer, mockEngine);
- mockSecurityContext.getUserID.returns('bob1');
+ mockSecurityContext.getIdentity.returns(identity);
mockSecurityContext.getChaincodeID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f');
mockEngine.query.resolves({ test: 'data from engine' });
return connection.queryChainCode(mockSecurityContext, 'testFunction', ['arg1', 'arg2'])
@@ -253,7 +249,7 @@ describe('EmbeddedConnection', () => {
sinon.assert.calledOnce(mockEngine.query);
sinon.assert.calledWith(mockEngine.query, sinon.match((context) => {
context.should.be.an.instanceOf(Context);
- context.getIdentityService().getCurrentUserID().should.equal('bob1');
+ context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
return true;
}), 'testFunction', ['arg1', 'arg2']);
result.should.be.an.instanceOf(Buffer);
@@ -271,7 +267,7 @@ describe('EmbeddedConnection', () => {
mockEngine.getContainer.returns(mockContainer);
EmbeddedConnection.addBusinessNetwork('org.acme.Business', 'devFabric1', '6eeb8858-eced-4a32-b1cd-2491f1e3718f');
EmbeddedConnection.addChaincode('6eeb8858-eced-4a32-b1cd-2491f1e3718f', mockContainer, mockEngine);
- mockSecurityContext.getUserID.returns('bob1');
+ mockSecurityContext.getIdentity.returns(identity);
mockSecurityContext.getChaincodeID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f');
mockEngine.invoke.resolves({ test: 'data from engine' });
return connection.invokeChainCode(mockSecurityContext, 'testFunction', ['arg1', 'arg2'])
@@ -279,7 +275,7 @@ describe('EmbeddedConnection', () => {
sinon.assert.calledOnce(mockEngine.invoke);
sinon.assert.calledWith(mockEngine.invoke, sinon.match((context) => {
context.should.be.an.instanceOf(Context);
- context.getIdentityService().getCurrentUserID().should.equal('bob1');
+ context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
return true;
}), 'testFunction', ['arg1', 'arg2']);
should.equal(result, undefined);
@@ -296,21 +292,10 @@ describe('EmbeddedConnection', () => {
beforeEach(() => {
connection.dataService = mockDataService = sinon.createStubInstance(DataService);
mockIdentitiesDataCollection = sinon.createStubInstance(DataCollection);
-
});
- it('should create and return the identities collection if it does not exist', () => {
- mockDataService.existsCollection.withArgs('identities').resolves(false);
- mockDataService.createCollection.withArgs('identities').resolves(mockIdentitiesDataCollection);
- return connection.getIdentities()
- .then((identities) => {
- identities.should.equal(mockIdentitiesDataCollection);
- });
- });
-
- it('should return the existing identities collection if it already exists', () => {
- mockDataService.existsCollection.withArgs('identities').resolves(true);
- mockDataService.getCollection.withArgs('identities').resolves(mockIdentitiesDataCollection);
+ it('should ensure and return the identities collection', () => {
+ mockDataService.ensureCollection.withArgs('identities').resolves(mockIdentitiesDataCollection);
return connection.getIdentities()
.then((identities) => {
identities.should.equal(mockIdentitiesDataCollection);
@@ -319,34 +304,57 @@ describe('EmbeddedConnection', () => {
});
- describe('#testIdentity', () => {
+ describe('#getIdentity', () => {
let mockIdentitiesDataCollection;
beforeEach(() => {
mockIdentitiesDataCollection = sinon.createStubInstance(DataCollection);
- sandbox.stub(connection, 'getIdentities').resolves(mockIdentitiesDataCollection);
+ sinon.stub(connection, 'getIdentities').resolves(mockIdentitiesDataCollection);
});
- it('should resolve if the user ID is admin', () => {
- return connection.testIdentity('admin', 'password');
+ it('should return the hardcoded admin identity', () => {
+ return connection.getIdentity('admin')
+ .should.eventually.be.deep.equal({
+ identifier: '',
+ name: 'admin',
+ issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e',
+ secret: 'adminpw'
+ });
});
- it('should resolve if the user ID exists', () => {
- mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'password' });
- return connection.testIdentity('doge', 'password');
+ it('should return the specified identity', () => {
+ mockIdentitiesDataCollection.get.withArgs('bob1').resolves(identity);
+ return connection.getIdentity('bob1')
+ .should.eventually.be.equal(identity);
+ });
+
+ });
+
+ describe('#testIdentity', () => {
+
+ it('should not check the secret if the name is admin', () => {
+ const identity = {
+ identifier: '',
+ name: 'admin',
+ issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e',
+ secret: 'adminpw'
+ };
+ sinon.stub(connection, 'getIdentity').resolves(identity);
+ return connection.testIdentity('admin', 'blahblah')
+ .should.eventually.be.equal(identity);
});
- it('should throw an error if the user ID does not exist', () => {
- mockIdentitiesDataCollection.get.withArgs('doge').rejects(new Error('such error'));
- return connection.testIdentity('doge', 'password')
- .should.be.rejectedWith(/such error/);
+ it('should throw if the secret does not match', () => {
+ sinon.stub(connection, 'getIdentity').resolves(identity);
+ return connection.testIdentity('bob1', 'blahblah')
+ .should.be.rejectedWith(/The secret blahblah specified for the identity bob1 does not match the stored secret suchsecret/);
});
- it('should throw an error if the user secret does not match', () => {
- mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'not correct' });
- return connection.testIdentity('doge', 'password')
- .should.be.rejectedWith(/ does not match/);
+ it('should not throw if the secret does match', () => {
+ sinon.stub(connection, 'getIdentity').resolves(identity);
+ return connection.testIdentity('bob1', 'suchsecret')
+ .should.eventually.be.equal(identity);
});
});
@@ -365,6 +373,7 @@ describe('EmbeddedConnection', () => {
mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'password' });
return connection.createIdentity(mockSecurityContext, 'doge')
.then((result) => {
+ sinon.assert.notCalled(mockIdentitiesDataCollection.add);
result.should.be.deep.equal({ userID: 'doge', userSecret: 'password' });
});
});
@@ -375,6 +384,14 @@ describe('EmbeddedConnection', () => {
mockIdentitiesDataCollection.add.withArgs('doge').resolves();
return connection.createIdentity(mockSecurityContext, 'doge')
.then((result) => {
+ sinon.assert.calledOnce(mockIdentitiesDataCollection.add);
+ sinon.assert.calledWith(mockIdentitiesDataCollection.add, 'doge', {
+ certificate: '',
+ identifier: '8f00d1b8319abc0ad87ccb6c1baae0a54c406c921c01e1ed165c33b93f3e5b6a',
+ issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e',
+ name: 'doge',
+ secret: 'f892c30a'
+ });
result.should.be.deep.equal({ userID: 'doge', userSecret: 'f892c30a' });
});
});
diff --git a/packages/composer-connector-embedded/test/embeddedsecuritycontext.js b/packages/composer-connector-embedded/test/embeddedsecuritycontext.js
index b89899e4d0..83a0b704df 100644
--- a/packages/composer-connector-embedded/test/embeddedsecuritycontext.js
+++ b/packages/composer-connector-embedded/test/embeddedsecuritycontext.js
@@ -23,27 +23,35 @@ const sinon = require('sinon');
describe('EmbeddedSecurityContext', () => {
+ const identity = {
+ identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a',
+ name: 'bob1',
+ issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665',
+ secret: 'suchsecret',
+ certificate: ''
+ };
+
let mockConnection;
+ let securityContext;
beforeEach(() => {
mockConnection = sinon.createStubInstance(Connection);
+ securityContext = new EmbeddedSecurityContext(mockConnection, identity);
});
describe('#constructor', () => {
it('should construct a new security context', () => {
- let securityContext = new EmbeddedSecurityContext(mockConnection, 'bob1');
securityContext.should.be.an.instanceOf(SecurityContext);
- securityContext.userID.should.equal('bob1');
+ securityContext.identity.should.equal(identity);
});
});
- describe('#getUserID', () => {
+ describe('#getIdentity', () => {
it('should get the current user ID', () => {
- let securityContext = new EmbeddedSecurityContext(mockConnection, 'bob1');
- securityContext.getUserID().should.equal('bob1');
+ securityContext.getIdentity().should.deep.equal(identity);
});
});
@@ -51,7 +59,6 @@ describe('EmbeddedSecurityContext', () => {
describe('#getChaincodeID', () => {
it('should get the chaincode ID', () => {
- let securityContext = new EmbeddedSecurityContext(mockConnection, 'bob1');
securityContext.chaincodeID = 'ed916d6a-21af-4a2a-a9be-a86f69aa641b';
securityContext.getChaincodeID().should.equal('ed916d6a-21af-4a2a-a9be-a86f69aa641b');
});
@@ -61,7 +68,6 @@ describe('EmbeddedSecurityContext', () => {
describe('#setChaincodeID', () => {
it('should set the chaincode ID', () => {
- let securityContext = new EmbeddedSecurityContext(mockConnection, 'bob1');
securityContext.setChaincodeID('ed916d6a-21af-4a2a-a9be-a86f69aa641b');
securityContext.chaincodeID.should.equal('ed916d6a-21af-4a2a-a9be-a86f69aa641b');
});
diff --git a/packages/composer-connector-web/lib/webconnection.js b/packages/composer-connector-web/lib/webconnection.js
index c62debdf06..86c241f936 100644
--- a/packages/composer-connector-web/lib/webconnection.js
+++ b/packages/composer-connector-web/lib/webconnection.js
@@ -14,8 +14,8 @@
'use strict';
-// const Resource = require('composer-common').Resource;
const Connection = require('composer-common').Connection;
+const createHash = require('sha.js');
const Engine = require('composer-runtime').Engine;
const uuid = require('uuid');
const WebContainer = require('composer-runtime-web').WebContainer;
@@ -29,6 +29,9 @@ const businessNetworks = {};
// A mapping of chaincode IDs to their instance objects.
const chaincodes = {};
+// The issuer for all identities.
+const DEFAULT_ISSUER = createHash('sha256').update('org1').digest('hex');
+
/**
* Base class representing a connection to a business network.
* @protected
@@ -150,30 +153,30 @@ class WebConnection extends Connection {
* object representing the logged in participant, or rejected with a login error.
*/
login(enrollmentID, enrollmentSecret) {
- // The 'admin' ID is special for the moment as it is not bound to a participant.
- let result = new WebSecurityContext(this, enrollmentID !== 'admin' ? enrollmentID : null);
if (!this.businessNetworkIdentifier) {
return this.testIdentity(enrollmentID, enrollmentSecret)
- .then(() => {
- return result;
+ .then((identity) => {
+ return new WebSecurityContext(this, identity);
});
}
+ let identity;
return this.testIdentity(enrollmentID, enrollmentSecret)
- .then(() => {
+ .then((identity_) => {
+ identity = identity_;
return this.getChaincodeID(this.businessNetworkIdentifier);
})
.then((chaincodeID) => {
- if (chaincodeID) {
- if (!WebConnection.getBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile)) {
- let container = WebConnection.createContainer(chaincodeID);
- let engine = WebConnection.createEngine(container);
- WebConnection.addBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile, chaincodeID);
- WebConnection.addChaincode(chaincodeID, container, engine);
- }
- result.setChaincodeID(chaincodeID);
- } else {
+ if (!chaincodeID) {
throw new Error(`No chaincode ID found for business network '${this.businessNetworkIdentifier}'`);
}
+ if (!WebConnection.getBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile)) {
+ let container = WebConnection.createContainer(chaincodeID);
+ let engine = WebConnection.createEngine(container);
+ WebConnection.addBusinessNetwork(this.businessNetworkIdentifier, this.connectionProfile, chaincodeID);
+ WebConnection.addChaincode(chaincodeID, container, engine);
+ }
+ const result = new WebSecurityContext(this, identity);
+ result.setChaincodeID(chaincodeID);
return result;
});
}
@@ -188,12 +191,12 @@ class WebConnection extends Connection {
*/
deploy(securityContext, businessNetwork, deployOptions) {
let container = WebConnection.createContainer();
- let userID = securityContext.getUserID();
+ let identity = securityContext.getIdentity();
let chaincodeID = container.getUUID();
let engine = WebConnection.createEngine(container);
WebConnection.addBusinessNetwork(businessNetwork.getName(), this.connectionProfile, chaincodeID);
WebConnection.addChaincode(chaincodeID, container, engine);
- let context = new WebContext(engine, userID, this);
+ let context = new WebContext(engine, identity, this);
return businessNetwork.toArchive()
.then((businessNetworkArchive) => {
const initArgs = {};
@@ -258,10 +261,10 @@ class WebConnection extends Connection {
* chaincode function once it has been invoked, or rejected with an error.
*/
queryChainCode(securityContext, functionName, args) {
- let userID = securityContext.getUserID();
+ let identity = securityContext.getIdentity();
let chaincodeID = securityContext.getChaincodeID();
let chaincode = WebConnection.getChaincode(chaincodeID);
- let context = new WebContext(chaincode.engine, userID, this);
+ let context = new WebContext(chaincode.engine, identity, this);
return chaincode.engine.query(context, functionName, args)
.then((data) => {
return Buffer.from(JSON.stringify(data));
@@ -277,10 +280,10 @@ class WebConnection extends Connection {
* has been invoked, or rejected with an error.
*/
invokeChainCode(securityContext, functionName, args) {
- let userID = securityContext.getUserID();
+ let identity = securityContext.getIdentity();
let chaincodeID = securityContext.getChaincodeID();
let chaincode = WebConnection.getChaincode(chaincodeID);
- let context = new WebContext(chaincode.engine, userID, this);
+ let context = new WebContext(chaincode.engine, identity, this);
return chaincode.engine.invoke(context, functionName, args)
.then((data) => {
return undefined;
@@ -289,47 +292,57 @@ class WebConnection extends Connection {
/**
* Get the data collection that stores identities.
- * @return {DataCollection} The data collection that stores identities.
+ * @return {Promise} A promise that is resolved with the data collection
+ * that stores identities.
*/
getIdentities() {
- return this.dataService.existsCollection('identities')
- .then((exists) => {
- if (exists) {
- return this.dataService.getCollection('identities');
- } else {
- return this.dataService.createCollection('identities');
- }
- });
+ return this.dataService.ensureCollection('identities');
}
/**
- * Test the specified user ID and secret to ensure that it is valid.
- * @param {string} userID The user ID.
- * @param {string} userSecret The user secret.
- * @return {Promise} A promise that is resolved if the user ID and secret
- * is valid, or rejected with an error.
+ * Get the identity for the specified name.
+ * @param {string} identityName The name for the identity.
+ * @return {Promise} A promise that is resolved with the identity, or
+ * rejected with an error.
*/
- testIdentity(userID, userSecret) {
- // The 'admin' ID is special for the moment as it is not bound to a participant.
- if (userID === 'admin') {
- return Promise.resolve();
+ getIdentity(identityName) {
+ if (identityName === 'admin') {
+ return Promise.resolve({
+ identifier: '',
+ name: 'admin',
+ issuer: DEFAULT_ISSUER,
+ secret: 'adminpw'
+ });
}
return this.getIdentities()
.then((identities) => {
- return identities.get(userID);
- })
+ return identities.get(identityName);
+ });
+ }
+
+ /**
+ * Test the specified identity name and secret to ensure that it is valid.
+ * @param {string} identityName The name for the identity.
+ * @param {string} identitySecret The secret for the identity.
+ * @return {Promise} A promise that is resolved if the user ID and secret
+ * is valid, or rejected with an error.
+ */
+ testIdentity(identityName, identitySecret) {
+ return this.getIdentity(identityName)
.then((identity) => {
- if (identity.userSecret !== userSecret) {
- throw new Error(`The user secret ${userSecret} specified for the user ID ${userID} does not match the stored user secret ${identity.userSecret}`);
+ if (identityName !== 'admin') {
+ if (identity.secret !== identitySecret) {
+ throw new Error(`The secret ${identitySecret} specified for the identity ${identityName} does not match the stored secret ${identity.secret}`);
+ }
}
+ return identity;
});
-
}
/**
- * Create a new identity for the specified user ID.
+ * Create a new identity for the specified name.
* @param {SecurityContext} securityContext The participant's security context.
- * @param {string} userID The user ID.
+ * @param {string} identityName The name for the new identity.
* @param {object} [options] Options for the new identity.
* @param {boolean} [options.issuer] Whether or not the new identity should have
* permissions to create additional new identities. False by default.
@@ -338,22 +351,32 @@ class WebConnection extends Connection {
* @return {Promise} A promise that is resolved with a generated user
* secret once the new identity has been created, or rejected with an error.
*/
- createIdentity(securityContext, userID, options) {
+ createIdentity(securityContext, identityName, options) {
let identities;
return this.getIdentities()
.then((identities_) => {
identities = identities_;
- return identities.exists(userID);
+ return identities.exists(identityName);
})
.then((exists) => {
if (exists) {
- return identities.get(userID);
+ return identities.get(identityName);
}
- const userSecret = uuid.v4().substring(0, 8);
- const identity = { userID: userID, userSecret: userSecret };
- return identities.add(userID, identity)
+ const identifier = createHash('sha256').update(uuid.v4()).digest('hex');
+ const secret = uuid.v4().substring(0, 8);
+ const identity = {
+ identifier,
+ name: identityName,
+ issuer: DEFAULT_ISSUER,
+ secret,
+ certificate: ''
+ };
+ return identities.add(identityName, identity)
.then(() => {
- return identity;
+ return {
+ userID: identity.name,
+ userSecret: identity.secret
+ };
});
});
}
diff --git a/packages/composer-connector-web/lib/websecuritycontext.js b/packages/composer-connector-web/lib/websecuritycontext.js
index 9681ede74b..e67195fe94 100644
--- a/packages/composer-connector-web/lib/websecuritycontext.js
+++ b/packages/composer-connector-web/lib/websecuritycontext.js
@@ -24,20 +24,20 @@ class WebSecurityContext extends SecurityContext {
/**
* Constructor.
* @param {Connection} connection The owning connection.
- * @param {String} userID The current user ID.
+ * @param {Object} identity The current identity.
*/
- constructor(connection, userID) {
+ constructor(connection, identity) {
super(connection);
- this.userID = userID;
+ this.identity = identity;
this.chaincodeID = null;
}
/**
- * Get the current user ID.
- * @return {string} The current user ID.
+ * Get the current identity.
+ * @return {string} The current identity.
*/
- getUserID() {
- return this.userID;
+ getIdentity() {
+ return this.identity;
}
/**
diff --git a/packages/composer-connector-web/test/webconnection.js b/packages/composer-connector-web/test/webconnection.js
index 1b1eb11dae..f1ffd4f9a2 100644
--- a/packages/composer-connector-web/test/webconnection.js
+++ b/packages/composer-connector-web/test/webconnection.js
@@ -37,6 +37,14 @@ require('sinon-as-promised');
describe('WebConnection', () => {
+ const identity = {
+ identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a',
+ name: 'bob1',
+ issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665',
+ secret: 'suchsecret',
+ certificate: ''
+ };
+
let sandbox;
let mockConnectionManager;
let mockConnectionProfileManager;
@@ -107,25 +115,13 @@ describe('WebConnection', () => {
sandbox.stub(connection, 'testIdentity').resolves();
});
- it('should return a new security context with a null user ID if the admin ID is specified', () => {
- connection = new WebConnection(mockConnectionManager, 'devFabric1');
- sandbox.stub(connection, 'testIdentity').resolves();
- return connection.login('admin', 'suchs3cret')
- .then((securityContext) => {
- securityContext.should.be.an.instanceOf(WebSecurityContext);
- should.equal(securityContext.getUserID(), null);
- should.equal(securityContext.getChaincodeID(), null);
- sinon.assert.calledWith(connection.testIdentity, 'admin', 'suchs3cret');
- });
- });
-
it('should return a new security context with a null chaincode ID if the business network was not specified', () => {
connection = new WebConnection(mockConnectionManager, 'devFabric1');
- sandbox.stub(connection, 'testIdentity').resolves();
+ sandbox.stub(connection, 'testIdentity').resolves(identity);
return connection.login('doge', 'suchs3cret')
.then((securityContext) => {
securityContext.should.be.an.instanceOf(WebSecurityContext);
- securityContext.getUserID().should.equal('doge');
+ securityContext.getIdentity().should.deep.equal(identity);
should.equal(securityContext.getChaincodeID(), null);
sinon.assert.calledWith(connection.testIdentity, 'doge', 'suchs3cret');
});
@@ -175,7 +171,7 @@ describe('WebConnection', () => {
mockBusinessNetwork.getName.returns('testnetwork');
let mockContainer = sinon.createStubInstance(WebContainer);
mockContainer.getUUID.returns('133c00a3-8555-4aa5-9165-9de9a8f8a838');
- mockSecurityContext.getUserID.returns('bob1');
+ mockSecurityContext.getIdentity.returns(identity);
sandbox.stub(WebConnection, 'createContainer').returns(mockContainer);
let mockEngine = sinon.createStubInstance(Engine);
mockEngine.getContainer.returns(mockContainer);
@@ -187,7 +183,7 @@ describe('WebConnection', () => {
sinon.assert.calledOnce(mockEngine.init);
sinon.assert.calledWith(mockEngine.init, sinon.match((context) => {
context.should.be.an.instanceOf(Context);
- context.getIdentityService().getCurrentUserID().should.equal('bob1');
+ context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
return true;
}), 'init', ['aGVsbG8gd29ybGQ=', '{}']);
sinon.assert.calledOnce(connection.ping);
@@ -277,7 +273,7 @@ describe('WebConnection', () => {
mockEngine.getContainer.returns(mockContainer);
WebConnection.addBusinessNetwork('org.acme.Business', 'devFabric1', '6eeb8858-eced-4a32-b1cd-2491f1e3718f');
WebConnection.addChaincode('6eeb8858-eced-4a32-b1cd-2491f1e3718f', mockContainer, mockEngine);
- mockSecurityContext.getUserID.returns('bob1');
+ mockSecurityContext.getIdentity.returns(identity);
mockSecurityContext.getChaincodeID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f');
mockEngine.query.resolves({ test: 'data from engine' });
return connection.queryChainCode(mockSecurityContext, 'testFunction', ['arg1', 'arg2'])
@@ -285,7 +281,7 @@ describe('WebConnection', () => {
sinon.assert.calledOnce(mockEngine.query);
sinon.assert.calledWith(mockEngine.query, sinon.match((context) => {
context.should.be.an.instanceOf(Context);
- context.getIdentityService().getCurrentUserID().should.equal('bob1');
+ context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
return true;
}), 'testFunction', ['arg1', 'arg2']);
result.should.be.an.instanceOf(Buffer);
@@ -303,7 +299,7 @@ describe('WebConnection', () => {
mockEngine.getContainer.returns(mockContainer);
WebConnection.addBusinessNetwork('org.acme.Business', 'devFabric1', '6eeb8858-eced-4a32-b1cd-2491f1e3718f');
WebConnection.addChaincode('6eeb8858-eced-4a32-b1cd-2491f1e3718f', mockContainer, mockEngine);
- mockSecurityContext.getUserID.returns('bob1');
+ mockSecurityContext.getIdentity.returns(identity);
mockSecurityContext.getChaincodeID.returns('6eeb8858-eced-4a32-b1cd-2491f1e3718f');
mockEngine.invoke.resolves({ test: 'data from engine' });
return connection.invokeChainCode(mockSecurityContext, 'testFunction', ['arg1', 'arg2'])
@@ -311,7 +307,7 @@ describe('WebConnection', () => {
sinon.assert.calledOnce(mockEngine.invoke);
sinon.assert.calledWith(mockEngine.invoke, sinon.match((context) => {
context.should.be.an.instanceOf(Context);
- context.getIdentityService().getCurrentUserID().should.equal('bob1');
+ context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
return true;
}), 'testFunction', ['arg1', 'arg2']);
should.equal(result, undefined);
@@ -328,57 +324,69 @@ describe('WebConnection', () => {
beforeEach(() => {
connection.dataService = mockDataService = sinon.createStubInstance(DataService);
mockIdentitiesDataCollection = sinon.createStubInstance(DataCollection);
-
- });
-
- it('should create and return the identities collection if it does not exist', () => {
- mockDataService.existsCollection.withArgs('identities').resolves(false);
- mockDataService.createCollection.withArgs('identities').resolves(mockIdentitiesDataCollection);
- return connection.getIdentities()
- .then((identities) => {
- identities.should.equal(mockIdentitiesDataCollection);
- });
});
- it('should return the existing identities collection if it already exists', () => {
- mockDataService.existsCollection.withArgs('identities').resolves(true);
- mockDataService.getCollection.withArgs('identities').resolves(mockIdentitiesDataCollection);
+ it('should ensure and return the identities collection', () => {
+ mockDataService.ensureCollection.withArgs('identities').resolves(mockIdentitiesDataCollection);
return connection.getIdentities()
- .then((identities) => {
- identities.should.equal(mockIdentitiesDataCollection);
- });
+ .then((identities) => {
+ identities.should.equal(mockIdentitiesDataCollection);
+ });
});
});
- describe('#testIdentity', () => {
+ describe('#getIdentity', () => {
let mockIdentitiesDataCollection;
beforeEach(() => {
mockIdentitiesDataCollection = sinon.createStubInstance(DataCollection);
- sandbox.stub(connection, 'getIdentities').resolves(mockIdentitiesDataCollection);
+ sinon.stub(connection, 'getIdentities').resolves(mockIdentitiesDataCollection);
});
- it('should resolve if the user ID is admin', () => {
- return connection.testIdentity('admin', 'password');
+ it('should return the hardcoded admin identity', () => {
+ return connection.getIdentity('admin')
+ .should.eventually.be.deep.equal({
+ identifier: '',
+ name: 'admin',
+ issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e',
+ secret: 'adminpw'
+ });
});
- it('should resolve if the user ID exists', () => {
- mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'password' });
- return connection.testIdentity('doge', 'password');
+ it('should return the specified identity', () => {
+ mockIdentitiesDataCollection.get.withArgs('bob1').resolves(identity);
+ return connection.getIdentity('bob1')
+ .should.eventually.be.equal(identity);
});
- it('should throw an error if the user ID does not exist', () => {
- mockIdentitiesDataCollection.get.withArgs('doge').rejects(new Error('such error'));
- return connection.testIdentity('doge', 'password')
- .should.be.rejectedWith(/such error/);
+ });
+
+ describe('#testIdentity', () => {
+
+ it('should not check the secret if the name is admin', () => {
+ const identity = {
+ identifier: '',
+ name: 'admin',
+ issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e',
+ secret: 'adminpw'
+ };
+ sinon.stub(connection, 'getIdentity').resolves(identity);
+ return connection.testIdentity('admin', 'blahblah')
+ .should.eventually.be.equal(identity);
});
- it('should throw an error if the user secret does not match', () => {
- mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'not correct' });
- return connection.testIdentity('doge', 'password')
- .should.be.rejectedWith(/ does not match/);
+ it('should throw if the secret does not match', () => {
+ sinon.stub(connection, 'getIdentity').resolves(identity);
+ return connection.testIdentity('bob1', 'blahblah')
+ .should.be.rejectedWith(/The secret blahblah specified for the identity bob1 does not match the stored secret suchsecret/);
+ });
+
+ it('should not throw if the secret does match', () => {
+ sinon.stub(connection, 'getIdentity').resolves(identity);
+ return connection.testIdentity('bob1', 'suchsecret')
+ .should.eventually.be.equal(identity);
});
});
@@ -397,6 +405,7 @@ describe('WebConnection', () => {
mockIdentitiesDataCollection.get.withArgs('doge').resolves({ userID: 'doge', userSecret: 'password' });
return connection.createIdentity(mockSecurityContext, 'doge')
.then((result) => {
+ sinon.assert.notCalled(mockIdentitiesDataCollection.add);
result.should.be.deep.equal({ userID: 'doge', userSecret: 'password' });
});
});
@@ -407,6 +416,14 @@ describe('WebConnection', () => {
mockIdentitiesDataCollection.add.withArgs('doge').resolves();
return connection.createIdentity(mockSecurityContext, 'doge')
.then((result) => {
+ sinon.assert.calledOnce(mockIdentitiesDataCollection.add);
+ sinon.assert.calledWith(mockIdentitiesDataCollection.add, 'doge', {
+ certificate: '',
+ identifier: '8f00d1b8319abc0ad87ccb6c1baae0a54c406c921c01e1ed165c33b93f3e5b6a',
+ issuer: '89e0c13fa652f52d91fc90d568b70070d6ed1a59c5d9f452dfb1b2a199b1928e',
+ name: 'doge',
+ secret: 'f892c30a'
+ });
result.should.be.deep.equal({ userID: 'doge', userSecret: 'f892c30a' });
});
});
diff --git a/packages/composer-connector-web/test/websecuritycontext.js b/packages/composer-connector-web/test/websecuritycontext.js
index e08155c7d9..68c6256c50 100644
--- a/packages/composer-connector-web/test/websecuritycontext.js
+++ b/packages/composer-connector-web/test/websecuritycontext.js
@@ -23,27 +23,35 @@ const sinon = require('sinon');
describe('WebSecurityContext', () => {
+ const identity = {
+ identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a',
+ name: 'bob1',
+ issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665',
+ secret: 'suchsecret',
+ certificate: ''
+ };
+
let mockConnection;
+ let securityContext;
beforeEach(() => {
mockConnection = sinon.createStubInstance(Connection);
+ securityContext = new WebSecurityContext(mockConnection, identity);
});
describe('#constructor', () => {
it('should construct a new security context', () => {
- let securityContext = new WebSecurityContext(mockConnection, 'bob1');
securityContext.should.be.an.instanceOf(SecurityContext);
- securityContext.userID.should.equal('bob1');
+ securityContext.identity.should.equal(identity);
});
});
- describe('#getUserID', () => {
+ describe('#getIdentity', () => {
it('should get the current user ID', () => {
- let securityContext = new WebSecurityContext(mockConnection, 'bob1');
- securityContext.getUserID().should.equal('bob1');
+ securityContext.getIdentity().should.deep.equal(identity);
});
});
@@ -51,7 +59,6 @@ describe('WebSecurityContext', () => {
describe('#getChaincodeID', () => {
it('should get the chaincode ID', () => {
- let securityContext = new WebSecurityContext(mockConnection, 'bob1');
securityContext.chaincodeID = 'ed916d6a-21af-4a2a-a9be-a86f69aa641b';
securityContext.getChaincodeID().should.equal('ed916d6a-21af-4a2a-a9be-a86f69aa641b');
});
@@ -61,7 +68,6 @@ describe('WebSecurityContext', () => {
describe('#setChaincodeID', () => {
it('should set the chaincode ID', () => {
- let securityContext = new WebSecurityContext(mockConnection, 'bob1');
securityContext.setChaincodeID('ed916d6a-21af-4a2a-a9be-a86f69aa641b');
securityContext.chaincodeID.should.equal('ed916d6a-21af-4a2a-a9be-a86f69aa641b');
});
diff --git a/packages/composer-runtime-embedded/lib/embeddedcontext.js b/packages/composer-runtime-embedded/lib/embeddedcontext.js
index 12347cd70e..d690148a1e 100644
--- a/packages/composer-runtime-embedded/lib/embeddedcontext.js
+++ b/packages/composer-runtime-embedded/lib/embeddedcontext.js
@@ -30,13 +30,13 @@ class EmbeddedContext extends Context {
/**
* Constructor.
* @param {Engine} engine The owning engine.
- * @param {String} userID The current user ID.
+ * @param {Object} identity The current identity.
* @param {EventEmitter} eventSink The event emitter
*/
- constructor(engine, userID, eventSink) {
+ constructor(engine, identity, eventSink) {
super(engine);
this.dataService = new EmbeddedDataService(engine.getContainer().getUUID());
- this.identityService = new EmbeddedIdentityService(userID);
+ this.identityService = new EmbeddedIdentityService(identity);
this.eventSink = eventSink;
}
diff --git a/packages/composer-runtime-embedded/lib/embeddedidentityservice.js b/packages/composer-runtime-embedded/lib/embeddedidentityservice.js
index 0cff6accca..b963d9e275 100644
--- a/packages/composer-runtime-embedded/lib/embeddedidentityservice.js
+++ b/packages/composer-runtime-embedded/lib/embeddedidentityservice.js
@@ -24,20 +24,43 @@ class EmbeddedIdentityService extends IdentityService {
/**
* Constructor.
- * @param {String} userID The current user ID.
+ * @param {String} identity The current identity.
*/
- constructor(userID) {
+ constructor(identity) {
super();
- this.userID = userID;
+ this.identity = identity;
}
/**
- * Retrieve the current user ID.
- * @return {string} The current user ID, or null if the current user ID cannot
- * be determined or has not been specified.
+ * Get a unique identifier for the identity used to submit the transaction.
+ * @return {string} A unique identifier for the identity used to submit the transaction.
*/
- getCurrentUserID() {
- return this.userID;
+ getIdentifier() {
+ return this.identity.identifier;
+ }
+
+ /**
+ * Get the name of the identity used to submit the transaction.
+ * @return {string} The name of the identity used to submit the transaction.
+ */
+ getName() {
+ return this.identity.name;
+ }
+
+ /**
+ * Get the issuer of the identity used to submit the transaction.
+ * @return {string} The issuer of the identity used to submit the transaction.
+ */
+ getIssuer() {
+ return this.identity.issuer;
+ }
+
+ /**
+ * Get the certificate for the identity used to submit the transaction.
+ * @return {string} The certificate for the identity used to submit the transaction.
+ */
+ getCertificate() {
+ return this.identity.certificate;
}
}
diff --git a/packages/composer-runtime-embedded/test/embeddedcontext.js b/packages/composer-runtime-embedded/test/embeddedcontext.js
index 4a2081eda8..62663100e5 100644
--- a/packages/composer-runtime-embedded/test/embeddedcontext.js
+++ b/packages/composer-runtime-embedded/test/embeddedcontext.js
@@ -30,9 +30,18 @@ const sinon = require('sinon');
describe('EmbeddedContext', () => {
+ const identity = {
+ identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a',
+ name: 'bob1',
+ issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665',
+ secret: 'suchsecret',
+ certificate: ''
+ };
+
let mockEmbeddedContainer;
let mockSerializer;
let mockEngine;
+ let context;
beforeEach(() => {
mockEmbeddedContainer = sinon.createStubInstance(EmbeddedContainer);
@@ -40,12 +49,12 @@ describe('EmbeddedContext', () => {
mockEngine = sinon.createStubInstance(Engine);
mockEngine.getContainer.returns(mockEmbeddedContainer);
mockSerializer = sinon.createStubInstance(Serializer);
+ context = new EmbeddedContext(mockEngine, identity);
});
describe('#constructor', () => {
it('should construct a new context', () => {
- let context = new EmbeddedContext(mockEngine, 'bob1');
context.should.be.an.instanceOf(Context);
});
@@ -54,7 +63,6 @@ describe('EmbeddedContext', () => {
describe('#getDataService', () => {
it('should return the container data service', () => {
- let context = new EmbeddedContext(mockEngine, 'bob1');
context.getDataService().should.be.an.instanceOf(EmbeddedDataService);
});
@@ -63,9 +71,8 @@ describe('EmbeddedContext', () => {
describe('#getIdentityService', () => {
it('should return the container identity service', () => {
- let context = new EmbeddedContext(mockEngine, 'bob1');
context.getIdentityService().should.be.an.instanceOf(EmbeddedIdentityService);
- context.getIdentityService().getCurrentUserID().should.equal('bob1');
+ context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
});
});
@@ -73,14 +80,12 @@ describe('EmbeddedContext', () => {
describe('#getEventService', () => {
it('should return the container event service', () => {
- let context = new EmbeddedContext(mockEngine, 'bob1');
context.getSerializer = sinon.stub().returns(mockSerializer);
context.getEventService().should.be.an.instanceOf(EmbeddedEventService);
});
it('should return the container event service if it is set', () => {
const mockEmbeddedEventService = sinon.createStubInstance(EmbeddedEventService);
- let context = new EmbeddedContext(mockEngine, 'bob1');
context.eventService = mockEmbeddedEventService;
context.getEventService().should.equal(mockEmbeddedEventService);
});
@@ -89,13 +94,11 @@ describe('EmbeddedContext', () => {
describe('#getHTTPService', () => {
it('should return the container HTTP service', () => {
- let context = new EmbeddedContext(mockEngine, 'bob1');
context.getHTTPService().should.be.an.instanceOf(EmbeddedHTTPService);
});
it('should return the container HTTP service if it is set', () => {
const mockEmbeddedHTTPService = sinon.createStubInstance(EmbeddedHTTPService);
- let context = new EmbeddedContext(mockEngine, 'bob1');
context.httpService = mockEmbeddedHTTPService;
context.getHTTPService().should.equal(mockEmbeddedHTTPService);
});
@@ -104,13 +107,11 @@ describe('EmbeddedContext', () => {
describe('#getScriptCompiler', () => {
it('should return the container script compiler', () => {
- let context = new EmbeddedContext(mockEngine, 'bob1');
context.getScriptCompiler().should.be.an.instanceOf(EmbeddedScriptCompiler);
});
it('should return the container script compiler if it is set', () => {
const mockEmbeddedScriptCompiler = sinon.createStubInstance(EmbeddedScriptCompiler);
- const context = new EmbeddedContext(mockEngine, 'bob1');
context.scriptCompiler = mockEmbeddedScriptCompiler;
context.getScriptCompiler().should.equal(mockEmbeddedScriptCompiler);
});
diff --git a/packages/composer-runtime-embedded/test/embeddedidentityservice.js b/packages/composer-runtime-embedded/test/embeddedidentityservice.js
index 6476eaeb3f..37cc3ef33a 100644
--- a/packages/composer-runtime-embedded/test/embeddedidentityservice.js
+++ b/packages/composer-runtime-embedded/test/embeddedidentityservice.js
@@ -22,11 +22,19 @@ const sinon = require('sinon');
describe('EmbeddedIdentityService', () => {
+ const identity = {
+ identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a',
+ name: 'bob1',
+ issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665',
+ secret: 'suchsecret',
+ certificate: ''
+ };
+
let identityService;
let sandbox;
beforeEach(() => {
- identityService = new EmbeddedIdentityService('bob1');
+ identityService = new EmbeddedIdentityService(identity);
sandbox = sinon.sandbox.create();
});
@@ -38,15 +46,48 @@ describe('EmbeddedIdentityService', () => {
it('should create a identity service', () => {
identityService.should.be.an.instanceOf(IdentityService);
- identityService.userID.should.equal('bob1');
+ identityService.identity.should.equal(identity);
+ });
+
+ });
+
+ describe('#constructor', () => {
+
+ it('should create a identity service', () => {
+ identityService.should.be.an.instanceOf(IdentityService);
+ identityService.identity.should.equal(identity);
+ });
+
+ });
+
+ describe('#getIdentifier', () => {
+
+ it('should return the identifier', () => {
+ should.equal(identityService.getIdentifier(), 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
+ });
+
+ });
+
+ describe('#getName', () => {
+
+ it('should return the name', () => {
+ should.equal(identityService.getName(), 'bob1');
+ });
+
+ });
+
+ describe('#getIssuer', () => {
+
+ it('should return the issuer', () => {
+ should.equal(identityService.getIssuer(), 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665');
});
});
- describe('#getCurrentUserID', () => {
+ describe('#getCertificate', () => {
- it('should return the current user ID', () => {
- should.equal(identityService.getCurrentUserID(), 'bob1');
+ it('should return the certificate', () => {
+ should.equal(identityService.getCertificate(), '');
});
});
diff --git a/packages/composer-runtime-web/lib/webcontext.js b/packages/composer-runtime-web/lib/webcontext.js
index eac1fca501..9af70f645c 100644
--- a/packages/composer-runtime-web/lib/webcontext.js
+++ b/packages/composer-runtime-web/lib/webcontext.js
@@ -29,13 +29,13 @@ class WebContext extends Context {
/**
* Constructor.
* @param {Engine} engine The owning engine.
- * @param {String} userID The current user ID.
+ * @param {Object} identity The current identity.
* @param {EventEmitter} eventSink The event emitter
*/
- constructor(engine, userID, eventSink) {
+ constructor(engine, identity, eventSink) {
super(engine);
this.dataService = new WebDataService(engine.getContainer().getUUID());
- this.identityService = new WebIdentityService(userID);
+ this.identityService = new WebIdentityService(identity);
this.eventSink = eventSink;
}
diff --git a/packages/composer-runtime-web/lib/webidentityservice.js b/packages/composer-runtime-web/lib/webidentityservice.js
index 3983081ba6..5295c309a9 100644
--- a/packages/composer-runtime-web/lib/webidentityservice.js
+++ b/packages/composer-runtime-web/lib/webidentityservice.js
@@ -24,20 +24,43 @@ class WebIdentityService extends IdentityService {
/**
* Constructor.
- * @param {String} userID The current user ID.
+ * @param {String} identity The current identity.
*/
- constructor(userID) {
+ constructor(identity) {
super();
- this.userID = userID;
+ this.identity = identity;
}
/**
- * Retrieve the current user ID.
- * @return {string} The current user ID, or null if the current user ID cannot
- * be determined or has not been specified.
+ * Get a unique identifier for the identity used to submit the transaction.
+ * @return {string} A unique identifier for the identity used to submit the transaction.
*/
- getCurrentUserID() {
- return this.userID;
+ getIdentifier() {
+ return this.identity.identifier;
+ }
+
+ /**
+ * Get the name of the identity used to submit the transaction.
+ * @return {string} The name of the identity used to submit the transaction.
+ */
+ getName() {
+ return this.identity.name;
+ }
+
+ /**
+ * Get the issuer of the identity used to submit the transaction.
+ * @return {string} The issuer of the identity used to submit the transaction.
+ */
+ getIssuer() {
+ return this.identity.issuer;
+ }
+
+ /**
+ * Get the certificate for the identity used to submit the transaction.
+ * @return {string} The certificate for the identity used to submit the transaction.
+ */
+ getCertificate() {
+ return this.identity.certificate;
}
}
diff --git a/packages/composer-runtime-web/test/webcontext.js b/packages/composer-runtime-web/test/webcontext.js
index 5b6da12773..62583aca5e 100644
--- a/packages/composer-runtime-web/test/webcontext.js
+++ b/packages/composer-runtime-web/test/webcontext.js
@@ -29,9 +29,18 @@ const sinon = require('sinon');
describe('WebContext', () => {
+ const identity = {
+ identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a',
+ name: 'bob1',
+ issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665',
+ secret: 'suchsecret',
+ certificate: ''
+ };
+
let mockWebContainer;
let mockSerializer;
let mockEngine;
+ let context;
beforeEach(() => {
mockWebContainer = sinon.createStubInstance(WebContainer);
@@ -39,12 +48,12 @@ describe('WebContext', () => {
mockEngine = sinon.createStubInstance(Engine);
mockEngine.getContainer.returns(mockWebContainer);
mockSerializer = sinon.createStubInstance(Serializer);
+ context = new WebContext(mockEngine, identity);
});
describe('#constructor', () => {
it('should construct a new context', () => {
- let context = new WebContext(mockEngine, 'bob1');
context.should.be.an.instanceOf(Context);
});
@@ -53,7 +62,6 @@ describe('WebContext', () => {
describe('#getDataService', () => {
it('should return the container logging service', () => {
- let context = new WebContext(mockEngine, 'bob1');
context.getDataService().should.be.an.instanceOf(WebDataService);
});
@@ -62,9 +70,8 @@ describe('WebContext', () => {
describe('#getIdentityService', () => {
it('should return the container identity service', () => {
- let context = new WebContext(mockEngine, 'bob1');
context.getIdentityService().should.be.an.instanceOf(WebIdentityService);
- context.getIdentityService().getCurrentUserID().should.equal('bob1');
+ context.getIdentityService().getIdentifier().should.equal('ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
});
});
@@ -72,14 +79,12 @@ describe('WebContext', () => {
describe('#getEventService', () => {
it('should return the container event service', () => {
- let context = new WebContext(mockEngine, 'bob1');
context.getSerializer = sinon.stub().returns(mockSerializer);
context.getEventService().should.be.an.instanceOf(WebEventService);
});
it('should return this.eventService if it is set', () => {
const mockWebEventService = sinon.createStubInstance(WebEventService);
- let context = new WebContext(mockEngine, 'bob1');
context.eventService = mockWebEventService;
context.getEventService().should.equal(mockWebEventService);
});
@@ -88,13 +93,11 @@ describe('WebContext', () => {
describe('#getHTTPService', () => {
it('should return the container http service', () => {
- let context = new WebContext(mockEngine, 'bob1');
context.getHTTPService().should.be.an.instanceOf(WebHTTPService);
});
it('should return this.httpService if it is set', () => {
const mockWebHTTPService = sinon.createStubInstance(WebHTTPService);
- let context = new WebContext(mockEngine, 'bob1');
context.httpService = mockWebHTTPService;
context.getHTTPService().should.equal(mockWebHTTPService);
});
diff --git a/packages/composer-runtime-web/test/webidentityservice.js b/packages/composer-runtime-web/test/webidentityservice.js
index 5c861b8525..a314a48b4d 100644
--- a/packages/composer-runtime-web/test/webidentityservice.js
+++ b/packages/composer-runtime-web/test/webidentityservice.js
@@ -22,11 +22,19 @@ const sinon = require('sinon');
describe('WebIdentityService', () => {
+ const identity = {
+ identifier: 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a',
+ name: 'bob1',
+ issuer: 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665',
+ secret: 'suchsecret',
+ certificate: ''
+ };
+
let identityService;
let sandbox;
beforeEach(() => {
- identityService = new WebIdentityService('bob1');
+ identityService = new WebIdentityService(identity);
sandbox = sinon.sandbox.create();
});
@@ -38,15 +46,39 @@ describe('WebIdentityService', () => {
it('should create a identity service', () => {
identityService.should.be.an.instanceOf(IdentityService);
- identityService.userID.should.equal('bob1');
+ identityService.identity.should.equal(identity);
+ });
+
+ });
+
+ describe('#getIdentifier', () => {
+
+ it('should return the identifier', () => {
+ should.equal(identityService.getIdentifier(), 'ae360f8a430cc34deb2a8901ef3efed7a2eed753d909032a009f6984607be65a');
+ });
+
+ });
+
+ describe('#getName', () => {
+
+ it('should return the name', () => {
+ should.equal(identityService.getName(), 'bob1');
+ });
+
+ });
+
+ describe('#getIssuer', () => {
+
+ it('should return the issuer', () => {
+ should.equal(identityService.getIssuer(), 'ce295bc0df46512670144b84af55f3d9a3e71b569b1e38baba3f032dc3000665');
});
});
- describe('#getCurrentUserID', () => {
+ describe('#getCertificate', () => {
- it('should return the current user ID', () => {
- should.equal(identityService.getCurrentUserID(), 'bob1');
+ it('should return the certificate', () => {
+ should.equal(identityService.getCertificate(), '');
});
});
diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js
index 3e8b264432..08f238dc09 100644
--- a/packages/composer-runtime/lib/context.js
+++ b/packages/composer-runtime/lib/context.js
@@ -105,7 +105,6 @@ class Context {
this.api = null;
this.queryExecutor = null;
this.identityManager = null;
- this.identity = null;
this.participant = null;
this.transaction = null;
this.accessController = null;
@@ -290,9 +289,6 @@ class Context {
return this.getIdentityManager().getIdentity()
.then((identity) => {
- // Save the current identity.
- this.identity = identity;
-
// Validate the identity.
try {
this.getIdentityManager().validateIdentity(identity);
@@ -300,7 +296,12 @@ class Context {
// Check for the case of activation required, and the user is trying to activate.
if (e.activationRequired && this.getFunction() === 'activateIdentity') {
- // Ignore.
+
+ // Don't throw the error as we are activating the identity, but return null
+ // so that the participant is not set because there is no current participant
+ // until the identity is activated and not revoked.
+ return null;
+
} else {
throw e;
}
diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js
index 60344d2f5a..ac19778530 100644
--- a/packages/composer-runtime/test/context.js
+++ b/packages/composer-runtime/test/context.js
@@ -310,7 +310,7 @@ describe('Context', () => {
error.activationRequired = true;
mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(error);
return context.loadCurrentParticipant()
- .should.eventually.be.equal(mockParticipant)
+ .should.eventually.be.null
.then(() => {
sinon.assert.calledOnce(mockIdentityManager.validateIdentity);
sinon.assert.calledWith(mockIdentityManager.validateIdentity, mockIdentity);
From 99b50dfc520903ffd71736a7297eaf991445f8cf Mon Sep 17 00:00:00 2001
From: Simon Stone
Date: Thu, 13 Jul 2017 08:18:07 +0100
Subject: [PATCH 3/7] Add composer identity bind command to CLI (#1547)
---
.../lib/cmds/identity/bindCommand.js | 40 +++++
.../lib/cmds/identity/issueCommand.js | 2 +-
.../lib/cmds/identity/lib/bind.js | 93 ++++++++++++
.../lib/cmds/identity/revokeCommand.js | 2 +-
packages/composer-cli/package.json | 8 +-
packages/composer-cli/test/identity/bind.js | 138 ++++++++++++++++++
packages/composer-runtime/lib/context.js | 10 +-
packages/composer-runtime/test/context.js | 18 +++
8 files changed, 301 insertions(+), 10 deletions(-)
create mode 100644 packages/composer-cli/lib/cmds/identity/bindCommand.js
create mode 100644 packages/composer-cli/lib/cmds/identity/lib/bind.js
create mode 100644 packages/composer-cli/test/identity/bind.js
diff --git a/packages/composer-cli/lib/cmds/identity/bindCommand.js b/packages/composer-cli/lib/cmds/identity/bindCommand.js
new file mode 100644
index 0000000000..9ff7b29a08
--- /dev/null
+++ b/packages/composer-cli/lib/cmds/identity/bindCommand.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const Bind = require ('./lib/bind.js');
+
+module.exports.command = 'bind [options]';
+module.exports.describe = 'Bind an existing identity to a participant in a participant registry';
+module.exports.builder = {
+ connectionProfileName: {alias: 'p', required: false, describe: 'The connection profile name', type: 'string' },
+ businessNetworkName: {alias: 'n', required: true, describe: 'The business network name', type: 'string' },
+ enrollId: { alias: 'i', required: true, describe: 'The enrollment ID of the user', type: 'string' },
+ enrollSecret: { alias: 's', required: false, describe: 'The enrollment secret of the user', type: 'string' },
+ participantId: { alias: 'a', required: true, describe: 'The particpant to issue the new identity to', type: 'string' },
+ publicKeyFile: { alias: 'c', required: true, describe: 'File containing the public key', type: 'string' }
+};
+
+module.exports.handler = (argv) => {
+
+ argv.thePromise = Bind.handler(argv)
+ .then(() => {
+ return;
+ })
+ .catch((error) => {
+ throw error;
+ });
+ return argv.thePromise;
+};
diff --git a/packages/composer-cli/lib/cmds/identity/issueCommand.js b/packages/composer-cli/lib/cmds/identity/issueCommand.js
index 181e09d590..f36a32bdb5 100644
--- a/packages/composer-cli/lib/cmds/identity/issueCommand.js
+++ b/packages/composer-cli/lib/cmds/identity/issueCommand.js
@@ -17,7 +17,7 @@
const Issue = require ('./lib/issue.js');
module.exports.command = 'issue [options]';
-module.exports.describe = 'Issue an identity to a participant in a participant registry';
+module.exports.describe = 'Issue a new identity to a participant in a participant registry';
module.exports.builder = {
connectionProfileName: {alias: 'p', required: false, describe: 'The connection profile name', type: 'string' },
businessNetworkName: {alias: 'n', required: true, describe: 'The business network name', type: 'string' },
diff --git a/packages/composer-cli/lib/cmds/identity/lib/bind.js b/packages/composer-cli/lib/cmds/identity/lib/bind.js
new file mode 100644
index 0000000000..701453b511
--- /dev/null
+++ b/packages/composer-cli/lib/cmds/identity/lib/bind.js
@@ -0,0 +1,93 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const cmdUtil = require('../../utils/cmdutils');
+const DEFAULT_PROFILE_NAME = 'defaultProfile';
+const fs = require('fs');
+
+/**
+ *
+ * Composer "identity bind" command
+ *
+ *
+ * @private
+ */
+class Bind {
+
+ /**
+ * Command process for deploy command
+ * @param {string} argv argument list from composer command
+ * @return {Promise} promise when command complete
+ */
+ static handler(argv) {
+ let businessNetworkConnection;
+ let enrollId;
+ let enrollSecret;
+ let connectionProfileName = Bind.getDefaultProfileName(argv);
+ let businessNetworkName;
+ let participantId = argv.participantId;
+ let publicKeyFile = argv.publicKeyFile;
+ let certificate;
+ try {
+ certificate = fs.readFileSync(publicKeyFile).toString();
+ } catch(error) {
+ return Promise.reject(new Error('Unable to read public key file ' + publicKeyFile + '. ' + error.message));
+ }
+
+ return (() => {
+ if (!argv.enrollSecret) {
+ return cmdUtil.prompt({
+ name: 'enrollmentSecret',
+ description: 'What is the enrollment secret of the user?',
+ required: true,
+ hidden: true,
+ replace: '*'
+ })
+ .then((result) => {
+ argv.enrollSecret = result;
+ });
+ } else {
+ return Promise.resolve();
+ }
+ })()
+ .then(() => {
+ enrollId = argv.enrollId;
+ enrollSecret = argv.enrollSecret;
+ businessNetworkName = argv.businessNetworkName;
+ businessNetworkConnection = cmdUtil.createBusinessNetworkConnection();
+ return businessNetworkConnection.connect(connectionProfileName, businessNetworkName, enrollId, enrollSecret);
+ })
+ .then(() => {
+ return businessNetworkConnection.bindIdentity(participantId, certificate);
+ })
+ .then((result) => {
+ console.log(`An identity was bound to the participant '${participantId}'`);
+ console.log('The participant can now connect to the business network using the identity');
+ });
+ }
+
+ /**
+ * Get default profile name
+ * @param {argv} argv program arguments
+ * @return {String} defaultConnection profile name
+ */
+ static getDefaultProfileName(argv) {
+ return argv.connectionProfileName || DEFAULT_PROFILE_NAME;
+ }
+
+}
+
+module.exports = Bind;
diff --git a/packages/composer-cli/lib/cmds/identity/revokeCommand.js b/packages/composer-cli/lib/cmds/identity/revokeCommand.js
index 6fdc8e9559..aa9239640d 100644
--- a/packages/composer-cli/lib/cmds/identity/revokeCommand.js
+++ b/packages/composer-cli/lib/cmds/identity/revokeCommand.js
@@ -17,7 +17,7 @@
const Revoke = require ('./lib/revoke.js');
module.exports.command = 'revoke [options]';
-module.exports.describe = 'Revoke an identity that was issued to a participant';
+module.exports.describe = 'Revoke an identity that was issued or bound to a participant';
module.exports.builder = {
connectionProfileName: {alias: 'p', required: false, describe: 'The connection profile name', type: 'string' },
businessNetworkName: {alias: 'n', required: true, describe: 'The business network name', type: 'string' },
diff --git a/packages/composer-cli/package.json b/packages/composer-cli/package.json
index 60efd18aa8..816736169c 100644
--- a/packages/composer-cli/package.json
+++ b/packages/composer-cli/package.json
@@ -83,9 +83,9 @@
],
"all": true,
"check-coverage": true,
- "statements": 60,
- "branches": 50,
- "functions": 58,
- "lines": 60
+ "statements": 70,
+ "branches": 56,
+ "functions": 62,
+ "lines": 70
}
}
diff --git a/packages/composer-cli/test/identity/bind.js b/packages/composer-cli/test/identity/bind.js
new file mode 100644
index 0000000000..15641a0ed9
--- /dev/null
+++ b/packages/composer-cli/test/identity/bind.js
@@ -0,0 +1,138 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const Client = require('composer-client');
+const BusinessNetworkConnection = Client.BusinessNetworkConnection;
+
+const Bind = require('../../lib/cmds/identity/bindCommand.js');
+const CmdUtil = require('../../lib/cmds/utils/cmdutils.js');
+
+const sinon = require('sinon');
+require('sinon-as-promised');
+const chai = require('chai');
+chai.should();
+chai.use(require('chai-as-promised'));
+
+const fs = require('fs');
+
+const BUSINESS_NETWORK_NAME = 'net.biz.TestNetwork-0.0.1';
+const DEFAULT_PROFILE_NAME = 'defaultProfile';
+const ENROLL_ID = 'SuccessKid';
+const ENROLL_SECRET = 'SuccessKidWin';
+
+describe('composer identity bind CLI unit tests', () => {
+
+ const pem = '-----BEGIN CERTIFICATE-----\nMIIB8TCCAZegAwIBAgIUKhzgF0nmP1Zl6uubdgq6wFohMC8wCgYIKoZIzj0EAwIw\nczELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh\nbiBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT\nE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNzEyMjIzNzAwWhcNMTgwNzEyMjIz\nNzAwWjAQMQ4wDAYDVQQDEwVhZG1pbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBPnG/X+v7R2ljdtRoreLnub9vlvU9BmF7LTuklTC0iHJr96ecQRkx6OCE8nDK09G\n5P6LvL95PUxlhDDdlStqASGjbDBqMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8E\nAjAAMB0GA1UdDgQWBBQl3QT+2qX5eTM2zaJ+MjU4vF+NQDArBgNVHSMEJDAigCAZ\nq2WruwSAfa0S5MCpqqZknnCGjjq9AhejItieR+GmrjAKBggqhkjOPQQDAgNIADBF\nAiEAok4RzwsOX7lSkUmGK+yfr9reJxvtyCHxQ68YC7blAQECIB3T3H0iwX+2MZaX\nmJucq33qkeHpFiq1focn3o6WAx/x\n-----END CERTIFICATE-----\n';
+
+ let sandbox;
+ let mockBusinessNetworkConnection;
+
+ beforeEach(() => {
+ sandbox = sinon.sandbox.create();
+ mockBusinessNetworkConnection = sinon.createStubInstance(BusinessNetworkConnection);
+ mockBusinessNetworkConnection.connect.resolves();
+ mockBusinessNetworkConnection.bindIdentity.withArgs('org.doge.Doge#DOGE_1', pem, sinon.match.object).resolves({
+ userID: 'dogeid1',
+ userSecret: 'suchsecret'
+ });
+ sandbox.stub(fs, 'readFileSync').withArgs('admin.pem').returns(pem);
+ sandbox.stub(CmdUtil, 'createBusinessNetworkConnection').returns(mockBusinessNetworkConnection);
+ sandbox.stub(process, 'exit');
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ it('should bind an existing identity using the default profile', () => {
+ let argv = {
+ businessNetworkName: BUSINESS_NETWORK_NAME,
+ enrollId: ENROLL_ID,
+ enrollSecret: ENROLL_SECRET,
+ participantId: 'org.doge.Doge#DOGE_1',
+ publicKeyFile: 'admin.pem'
+ };
+ return Bind.handler(argv)
+ .then((res) => {
+ sinon.assert.calledOnce(mockBusinessNetworkConnection.connect);
+ sinon.assert.calledWith(mockBusinessNetworkConnection.connect, DEFAULT_PROFILE_NAME, argv.businessNetworkName, argv.enrollId, argv.enrollSecret);
+ sinon.assert.calledOnce(mockBusinessNetworkConnection.bindIdentity);
+ sinon.assert.calledWith(mockBusinessNetworkConnection.bindIdentity, 'org.doge.Doge#DOGE_1', pem);
+ });
+ });
+
+ it('should bind an existing identity using the specified profile', () => {
+ let argv = {
+ connectionProfileName: 'someOtherProfile',
+ businessNetworkName: BUSINESS_NETWORK_NAME,
+ enrollId: ENROLL_ID,
+ enrollSecret: ENROLL_SECRET,
+ participantId: 'org.doge.Doge#DOGE_1',
+ publicKeyFile: 'admin.pem'
+ };
+ return Bind.handler(argv)
+ .then((res) => {
+ sinon.assert.calledOnce(mockBusinessNetworkConnection.connect);
+ sinon.assert.calledWith(mockBusinessNetworkConnection.connect, 'someOtherProfile', argv.businessNetworkName, argv.enrollId, argv.enrollSecret);
+ sinon.assert.calledOnce(mockBusinessNetworkConnection.bindIdentity);
+ sinon.assert.calledWith(mockBusinessNetworkConnection.bindIdentity, 'org.doge.Doge#DOGE_1', pem);
+ });
+ });
+
+ it('should prompt for the enrollment secret if not specified', () => {
+ sandbox.stub(CmdUtil, 'prompt').resolves(ENROLL_SECRET);
+ let argv = {
+ businessNetworkName: BUSINESS_NETWORK_NAME,
+ enrollId: ENROLL_ID,
+ participantId: 'org.doge.Doge#DOGE_1',
+ publicKeyFile: 'admin.pem'
+ };
+ return Bind.handler(argv)
+ .then((res) => {
+ sinon.assert.calledOnce(mockBusinessNetworkConnection.connect);
+ sinon.assert.calledWith(mockBusinessNetworkConnection.connect, DEFAULT_PROFILE_NAME, argv.businessNetworkName, argv.enrollId, argv.enrollSecret);
+ sinon.assert.calledOnce(mockBusinessNetworkConnection.bindIdentity);
+ sinon.assert.calledWith(mockBusinessNetworkConnection.bindIdentity, 'org.doge.Doge#DOGE_1', pem);
+ });
+ });
+
+ it('should error when the certificate file cannot be read', () => {
+ fs.readFileSync.withArgs('admin.pem').throws(new Error('such error'));
+ let argv = {
+ businessNetworkName: BUSINESS_NETWORK_NAME,
+ enrollId: ENROLL_ID,
+ enrollSecret: ENROLL_SECRET,
+ participantId: 'org.doge.Doge#DOGE_1',
+ publicKeyFile: 'admin.pem'
+ };
+ return Bind.handler(argv)
+ .should.be.rejectedWith(/such error/);
+ });
+
+ it('should error when the existing identity cannot be bound', () => {
+ mockBusinessNetworkConnection.bindIdentity.withArgs('org.doge.Doge#DOGE_1', pem).rejects(new Error('such error'));
+ let argv = {
+ businessNetworkName: BUSINESS_NETWORK_NAME,
+ enrollId: ENROLL_ID,
+ enrollSecret: ENROLL_SECRET,
+ participantId: 'org.doge.Doge#DOGE_1',
+ publicKeyFile: 'admin.pem'
+ };
+ return Bind.handler(argv)
+ .should.be.rejectedWith(/such error/);
+ });
+
+});
diff --git a/packages/composer-runtime/lib/context.js b/packages/composer-runtime/lib/context.js
index 08f238dc09..397dfca1dc 100644
--- a/packages/composer-runtime/lib/context.js
+++ b/packages/composer-runtime/lib/context.js
@@ -321,10 +321,12 @@ class Context {
// Check for an admin user.
// TODO: this is temporary whilst we migrate to requiring all
// users to have identities that are mapped to participants.
- const name = this.getIdentityService().getName();
- if (name && name.match(/admin/i)) {
- LOG.exit(method, null);
- return null;
+ if (!error.activationRequired) {
+ const name = this.getIdentityService().getName();
+ if (name && name.match(/admin/i)) {
+ LOG.exit(method, null);
+ return null;
+ }
}
// Throw the error.
diff --git a/packages/composer-runtime/test/context.js b/packages/composer-runtime/test/context.js
index ac19778530..ba8df7e850 100644
--- a/packages/composer-runtime/test/context.js
+++ b/packages/composer-runtime/test/context.js
@@ -336,6 +336,24 @@ describe('Context', () => {
.should.be.rejectedWith(/such error/);
});
+ it('should not ignore an activation required error from looking up the identity for admin users', () => {
+ mockIdentityManager.getIdentity.resolves(mockIdentity);
+ mockIdentityManager.getParticipant.withArgs(mockIdentity).resolves(mockParticipant);
+ context.function = 'bindIdentity';
+ const error = new Error('such error');
+ error.activationRequired = true;
+ mockIdentityManager.validateIdentity.withArgs(mockIdentity).throws(error);
+ let promise = Promise.resolve();
+ ['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => {
+ mockIdentityService.getName.returns(admin);
+ promise = promise.then(() => {
+ return context.loadCurrentParticipant()
+ .should.be.rejectedWith(/such error/);
+ });
+ });
+ return promise;
+ });
+
it('should ignore any errors from looking up the identity for admin users', () => {
let promise = Promise.resolve();
['admin', 'Admin', 'WebAppAdmin'].forEach((admin) => {
From ecb1c6517b2f9bbd56818588b81b924c3c79b6cf Mon Sep 17 00:00:00 2001
From: Liam Grace <14gracel@users.noreply.github.com>
Date: Thu, 13 Jul 2017 19:35:17 +0100
Subject: [PATCH 4/7] Identity registry APIs (#1553)
* Remove double scroll bar (#1527)
* Fix ability to issue identity to none existent participant (#1526)
* Prevent identifying fields in transaction and event model (#1517) (#1544)
* Fixed protractor tests
* Prevent identifying fields in transaction and event model
* Updated tutorial guide, minor fixes to cmdline , package.json instruction (#1534)
* Updated Developer Tutorial removed version info and minor errata and tidy up
* Updated Developer Tutorial package.json instruction
* Use workaround for jekyll issue #6221 (#1551)
* Merge PRs 1548 + 1550, pull e2e tests out into separate build (#1552)
* comment out the exit
* Move the PouchDB adapter plugin loading into the modules that need it
* Pull out e2e tests into separate Travis build
* Comment out e2e tests
* Initial drop of identity registry (#1539)
* Get identities working again on embedded and web runtimes (#1546)
* Cannot activate issued identity as identity not found
* Get identities working again on embedded and web runtimes
* Add composer identity bind command to CLI (#1547)
* Identity registry APIs
---
.travis.yml | 1 +
.travis/before-install.sh | 4 +-
.travis/script.sh | 6 +
.../data/businessnetwork/models/mozart.cto | 2 +-
packages/composer-client/api.txt | 4 +
packages/composer-client/changelog.txt | 3 +-
.../lib/businessnetworkconnection.js | 47 +-
.../composer-client/lib/identityregistry.js | 148 +++++
.../test/businessnetworkconnection.js | 95 ++++
.../composer-client/test/identityregistry.js | 193 +++++++
.../lib/introspect/classdeclaration.js | 11 +
.../lib/introspect/eventdeclaration.js | 14 +
.../lib/introspect/transactiondeclaration.js | 15 +
packages/composer-common/lib/system.cto | 2 +-
.../test/data/model/concept2.cto | 10 +-
.../model/dependencies/contract/proforma.cto | 4 +-
.../test/data/model/farm2fork.cto | 2 +-
.../classdeclaration.dupetransactionname.cto | 4 +-
.../test/introspect/functiondeclaration.js | 2 +-
.../test/introspect/transactiondeclaration.js | 13 +-
.../e2e/data/bna/basic-sample-network.bna | Bin 5574 -> 6444 bytes
.../e2e/tests/editor-define.spec.ts | 507 +++++++++---------
.../e2e/utils/add-file-helper.ts | 23 +-
.../e2e/utils/editor-helper.ts | 9 +-
.../e2e/utils/import-helper.ts | 10 +-
.../e2e/utils/operations-helper.ts | 2 +-
.../e2e/utils/replace-helper.ts | 4 +-
packages/composer-playground/package.json | 3 +-
.../src/app/app.component.html | 4 +-
.../src/app/footer/footer.component.scss | 5 +-
.../styles/components/_side-bar-nav.scss | 2 +-
.../lib/embeddeddataservice.js | 3 +
.../composer-runtime-embedded/package.json | 1 +
.../lib/pouchdbdataservice.js | 13 +-
.../composer-runtime-pouchdb/package.json | 3 -
.../test/pouchdbdataservice.js | 14 +
.../lib/webdataservice.js | 4 +
packages/composer-runtime-web/package.json | 2 +
.../composer-runtime/test/data/mozart.cto | 2 +-
.../composer-systests/systest/identities.js | 7 +
.../jekylldocs/tutorials/developer-guide.md | 31 +-
.../composer-website/scripts/setup-jekyll.sh | 2 +-
.../templates/models/namespace.cto | 2 +-
.../model/templates/models/namespace.cto | 2 +-
.../test/businessnetworkconnector.js | 3 +-
45 files changed, 888 insertions(+), 350 deletions(-)
create mode 100644 packages/composer-client/lib/identityregistry.js
create mode 100644 packages/composer-client/test/identityregistry.js
diff --git a/.travis.yml b/.travis.yml
index 90031641c5..3d9853d422 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,6 +8,7 @@ matrix:
- env: SYSTEST=embedded,proxy,web FC_TASK=systest
- env: SYSTEST=hlf FC_TASK=systest
- env: SYSTEST=hlfv1_tls FC_TASK=systest
+ # - env: SYSTEST=e2e FC_TASK=systest
dist: trusty
addons:
apt:
diff --git a/.travis/before-install.sh b/.travis/before-install.sh
index bb79105b1d..b06ea73376 100755
--- a/.travis/before-install.sh
+++ b/.travis/before-install.sh
@@ -99,8 +99,8 @@ then
if [ "${FC_TASK}" != "docs" ]; then
# echo "ABORT_BUILD=true" > ${DIR}/build.cfg
# echo "ABORT_CODE=0" >> ${DIR}/build.cfg
- echo 'Docs only build - no pointing bothering with anything more'
- exit 0
+ echo 'Docs only build'
+# exit 0
fi
else
diff --git a/.travis/script.sh b/.travis/script.sh
index aaddc3d978..98b544cc79 100755
--- a/.travis/script.sh
+++ b/.travis/script.sh
@@ -45,6 +45,12 @@ if [ "${DOCS}" != "" ]; then
npm run linkcheck:unstable
fi
+# Are we running playground e2e tests?
+elif [ "${SYSTEST}" = "e2e" ]; then
+
+ # Run the playground e2e tests.
+ cd "${DIR}/packages/composer-playground"
+ npm run e2e:main
# Are we running system tests?
elif [ "${SYSTEST}" != "" ]; then
diff --git a/packages/composer-admin/test/data/businessnetwork/models/mozart.cto b/packages/composer-admin/test/data/businessnetwork/models/mozart.cto
index 221437a2be..76e47aed01 100644
--- a/packages/composer-admin/test/data/businessnetwork/models/mozart.cto
+++ b/packages/composer-admin/test/data/businessnetwork/models/mozart.cto
@@ -103,7 +103,7 @@ asset Business identified by sbi {
/**
* An abstract transaction type for animal movements
*/
-abstract transaction AnimalMovement identified by transactionId {
+abstract transaction AnimalMovement {
o String transactionId
o String[] logs optional
--> Animal animal
diff --git a/packages/composer-client/api.txt b/packages/composer-client/api.txt
index 59b72cc7e8..c89670c5d7 100644
--- a/packages/composer-client/api.txt
+++ b/packages/composer-client/api.txt
@@ -17,6 +17,7 @@ class BusinessNetworkConnection extends EventEmitter {
+ Promise participantRegistryExists(string)
+ Promise addParticipantRegistry(string,string)
+ Promise getTransactionRegistry()
+ + Promise getIdentityRegistry()
+ Promise connect(string,string,string,string,Object)
+ Promise disconnect()
+ Promise submitTransaction(Resource)
@@ -27,6 +28,9 @@ class BusinessNetworkConnection extends EventEmitter {
+ Promise bindIdentity(string)
+ Promise revokeIdentity(string)
}
+class IdentityRegistry extends Registry {
+ + Promise getIdentityRegistry(SecurityContext,ModelManager,Factory,Serializer)
+}
class ParticipantRegistry extends Registry {
+ Promise getAllParticipantRegistries(SecurityContext,ModelManager,Factory,Serializer)
+ Promise getParticipantRegistry(SecurityContext,string,ModelManager,Factory,Serializer)
diff --git a/packages/composer-client/changelog.txt b/packages/composer-client/changelog.txt
index 42108c1389..600b3f99b2 100644
--- a/packages/composer-client/changelog.txt
+++ b/packages/composer-client/changelog.txt
@@ -12,8 +12,9 @@
# Note that the latest public API is documented using JSDocs and is available in api.txt.
#
-Version 0.9.2 {465d5a96640dce8240a392d058e06fe5} 2017-07-11
+Version 0.9.2 {60d41f6d3b545fd4ad53ca2bf0e5a36c} 2017-07-11
- Added bindIdentity
+- Added getIdentityRegistry
Version 0.9.0 {d6ebf2b722345ee0f7dac56f42730f12} 2017-06-26
- Added buildQuery and query
diff --git a/packages/composer-client/lib/businessnetworkconnection.js b/packages/composer-client/lib/businessnetworkconnection.js
index bace44d451..51901864cb 100644
--- a/packages/composer-client/lib/businessnetworkconnection.js
+++ b/packages/composer-client/lib/businessnetworkconnection.js
@@ -28,6 +28,7 @@ const Query = require('./query');
const Resource = require('composer-common').Resource;
const TransactionDeclaration = require('composer-common').TransactionDeclaration;
const TransactionRegistry = require('./transactionregistry');
+const IdentityRegistry = require('./identityregistry');
const Util = require('composer-common').Util;
const uuid = require('uuid');
@@ -288,6 +289,33 @@ class BusinessNetworkConnection extends EventEmitter {
});
}
+ /**
+ * Get the identity registry.
+ * @example
+ * // Get the transaction registry
+ * var businessNetwork = new BusinessNetworkConnection();
+ * return businessNetwork.connect('testprofile', 'businessNetworkIdentifier', 'WebAppAdmin', 'DJY27pEnl16d')
+ * .then(function(businessNetworkDefinition){
+ * return businessNetworkDefinition.getIdentityRegistry();
+ * })
+ * .then(function(identityRegistry){
+ * // Retrieved Identity Registry
+ * });
+ * @return {Promise} - A promise that will be resolved to the {@link IdentityRegistry}
+ */
+ getIdentityRegistry() {
+ Util.securityCheck(this.securityContext);
+ return IdentityRegistry
+ .getIdentityRegistry(this.securityContext, this.getBusinessNetwork().getModelManager(), this.getBusinessNetwork().getFactory(), this.getBusinessNetwork().getSerializer())
+ .then((identityRegistry) => {
+ if (identityRegistry) {
+ return identityRegistry;
+ } else {
+ throw new Error('Failed to find the default identity registry');
+ }
+ });
+ }
+
/**
* Connects to a business network using a connection profile, and authenticates to the Hyperledger Fabric.
* @example
@@ -600,13 +628,30 @@ class BusinessNetworkConnection extends EventEmitter {
throw new Error('identityName not specified');
}
let participantFQI;
+ let participantId;
+ let participantType;
if (participant instanceof Resource) {
participantFQI = participant.getFullyQualifiedIdentifier();
+ participantId = participant.getIdentifier();
+ participantType = participant.getFullyQualifiedType();
} else {
participantFQI = participant;
+ participantId = participantFQI.substring(participantFQI.lastIndexOf('#') + 1);
+ participantType = participantFQI.substr(0, participantFQI.lastIndexOf('#'));
}
+
Util.securityCheck(this.securityContext);
- return this.connection.createIdentity(this.securityContext, identityName, options)
+ return this.getParticipantRegistry(participantType)
+ .then((participantRegistry) => {
+ return participantRegistry.exists(participantId);
+ })
+ .then((exists) => {
+ if (exists) {
+ return this.connection.createIdentity(this.securityContext, identityName, options);
+ } else {
+ throw new Error(`Participant '${participantFQI}' does not exist `);
+ }
+ })
.then((identity) => {
return Util.invokeChainCode(this.securityContext, 'issueIdentity', [participantFQI, identityName])
.then(() => {
diff --git a/packages/composer-client/lib/identityregistry.js b/packages/composer-client/lib/identityregistry.js
new file mode 100644
index 0000000000..e681942f83
--- /dev/null
+++ b/packages/composer-client/lib/identityregistry.js
@@ -0,0 +1,148 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const Registry = require('./registry');
+const Util = require('composer-common').Util;
+
+const REGISTRY_TYPE = 'Asset';
+
+/**
+ * The IdentityRegistry is used to store a set of identities on the blockchain.
+ *
+ * @extends Registry
+ * @see See [Registry]{@link module:composer-client.Registry}
+ * @class
+ * @memberof module:composer-client
+ */
+class IdentityRegistry extends Registry {
+
+ /**
+ * Get an existing identity registry.
+ *
+ * @param {SecurityContext} securityContext The user's security context.
+ * @param {ModelManager} modelManager The ModelManager to use for this identity registry.
+ * @param {Factory} factory The factory to use for this identity registry.
+ * @param {Serializer} serializer The Serializer to use for this identity registry.
+ * @return {Promise} A promise that will be resolved with a {@link IdentityRegistry}
+ * instance representing the identity registry.
+ */
+ static getIdentityRegistry(securityContext, modelManager, factory, serializer) {
+ Util.securityCheck(securityContext);
+ if (!modelManager) {
+ throw new Error('modelManager not specified');
+ } else if (!factory) {
+ throw new Error('factory not specified');
+ } else if (!serializer) {
+ throw new Error('serializer not specified');
+ }
+ return Registry.getAllRegistries(securityContext, REGISTRY_TYPE)
+ .then((registries) => {
+ return registries.map((registry) => {
+ if (registry.id === 'org.hyperledger.composer.system.Identity') {
+ return new IdentityRegistry(registry.id, registry.name, securityContext, modelManager, factory, serializer);
+ }
+ })[0];
+ });
+ }
+
+ /**
+ * Create an identity registry.
+ * Note: Only to be called by framework code. Applications should
+ * retrieve instances from {@link BusinessNetworkConnection}
+ *
+ *
+ * @param {string} id The unique identifier of the identity registry.
+ * @param {string} name The display name for the identity registry.
+ * @param {SecurityContext} securityContext The security context to use for this asset registry.
+ * @param {ModelManager} modelManager The ModelManager to use for this identity registry.
+ * @param {Factory} factory The factory to use for this identity registry.
+ * @param {Serializer} serializer The Serializer to use for this identity registry.
+ * @private
+ */
+ constructor(id, name, securityContext, modelManager, factory, serializer) {
+ super(REGISTRY_TYPE, id, name, securityContext, modelManager, factory, serializer);
+ }
+
+ /**
+ * Unsupported operation; you cannot add an identity to an identity
+ * registry.
+ *
+ * @param {Resource} resource The resource to be added to the registry.
+ * @param {string} data The data for the resource.
+ * @private
+ */
+ add(resource) {
+ throw new Error('cannot add identity to an identity registry');
+ }
+
+ /**
+ * Unsupported operation; you cannot add an identity to an identity
+ * registry.
+ *
+ * @param {Resource[]} resources The resources to be added to the registry.
+ * @private
+ */
+ addAll(resources) {
+ throw new Error('cannot add identities to a identity registry');
+ }
+
+ /**
+ * Unsupported operation; you cannot update an identity in an identity
+ * registry. This method will always throw an exception when called.
+ *
+ * @param {Resource} resource The resource to be updated in the registry.
+ * @private
+ */
+ update(resource) {
+ throw new Error('cannot update identities in an identity registry');
+ }
+
+ /**
+ * Unsupported operation; you cannot update an identity in an identity
+ * registry.
+ *
+ * @param {Resource[]} resources The resources to be updated in the asset registry.
+ * @private
+ */
+ updateAll(resources) {
+ throw new Error('cannot update identities in an identity registry');
+ }
+
+ /**
+ * Unsupported operation; you cannot remove an identity from an identity
+ * registry. This method will always throw an exception when called.
+ *
+ * @param {(Resource|string)} resource The resource, or the unique identifier of the resource.
+ * @private
+ */
+ remove(resource) {
+ throw new Error('cannot remove identities from an identity registry');
+ }
+
+ /**
+ * Unsupported operation; you cannot remove an identity from an identity
+ * registry. This method will always throw an exception when called.
+ *
+ * @param {(Resource[]|string[])} resources The resources, or the unique identifiers of the resources.
+ * @private
+ */
+ removeAll(resources) {
+ throw new Error('cannot remove identities from an identity registry');
+ }
+
+}
+
+module.exports = IdentityRegistry;
diff --git a/packages/composer-client/test/businessnetworkconnection.js b/packages/composer-client/test/businessnetworkconnection.js
index 660c660d08..f58c48e1f5 100644
--- a/packages/composer-client/test/businessnetworkconnection.js
+++ b/packages/composer-client/test/businessnetworkconnection.js
@@ -33,6 +33,8 @@ const Resource = require('composer-common').Resource;
const SecurityContext = require('composer-common').SecurityContext;
const TransactionDeclaration = require('composer-common').TransactionDeclaration;
const TransactionRegistry = require('../lib/transactionregistry');
+const IdentityRegistry = require('../lib/identityregistry');
+const Registry = require('../lib/registry');
const Util = require('composer-common').Util;
const uuid = require('uuid');
const version = require('../package.json').version;
@@ -55,6 +57,7 @@ describe('BusinessNetworkConnection', () => {
let mockQueryFile;
let mockFactory;
let mockSerializer;
+ let mockParticipantRegistry;
beforeEach(() => {
sandbox = sinon.sandbox.create();
@@ -75,6 +78,7 @@ describe('BusinessNetworkConnection', () => {
mockSerializer = sinon.createStubInstance(Serializer);
businessNetworkConnection.businessNetwork.getSerializer.returns(mockSerializer);
businessNetworkConnection.securityContext = mockSecurityContext;
+ mockParticipantRegistry = sinon.createStubInstance(ParticipantRegistry);
delete process.env.COMPOSER_CONFIG;
});
@@ -946,6 +950,11 @@ describe('BusinessNetworkConnection', () => {
userID: 'dogeid1',
userSecret: 'suchsecret'
});
+
+ mockParticipantRegistry.exists.resolves(true);
+ businessNetworkConnection.getParticipantRegistry = () => {
+ return Promise.resolve(mockParticipantRegistry);
+ };
});
it('should throw if participant not specified', () => {
@@ -1009,6 +1018,59 @@ describe('BusinessNetworkConnection', () => {
});
});
+ it('should throw if participant does not exist', () => {
+ sandbox.stub(Util, 'invokeChainCode').resolves();
+ mockParticipantRegistry.exists.resolves(false);
+
+ return businessNetworkConnection.issueIdentity('org.doge.Doge#DOGE_1', 'dogeid1')
+ .catch((error) => {
+ error.should.match(/does not exist /);
+ });
+ });
+ });
+
+ describe('#bindIdentity', () => {
+
+ const pem = '-----BEGIN CERTIFICATE-----\nMIIB8jCCAZmgAwIBAgIULKt4c4xcdMwGgjNef9IL92HQkyAwCgYIKoZIzj0EAwIw\nczELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh\nbiBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT\nE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwNzA4MTg1NzAwWhcNMTgwNzA4MTg1\nNzAwWjASMRAwDgYDVQQDEwdib29iaWVzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD\nQgAE5P4RNqfEy8pArDxAbVIjRxqkwlpHUY7ANR6X7a4uvVIzIPDx4p7lf37xuc+5\nI9VZCvcI1SA5nIRphet0yYSgZaNsMGowDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwEB\n/wQCMAAwHQYDVR0OBBYEFAmjJfUZvdB8pHvklsdd1HiVog+VMCsGA1UdIwQkMCKA\nIBmrZau7BIB9rRLkwKmqpmSecIaOOr0CF6Mi2J5H4aauMAoGCCqGSM49BAMCA0cA\nMEQCIGtqR9rUR2ESu2UfUpNUfEeeBsshMkMHmuP/r5uvo2fSAiBtFB9Aid/3nexB\nI5qkVbdRSRQpt7uxoKFDLV/LUDM9xw==\n-----END CERTIFICATE-----\n';
+
+ beforeEach(() => {
+ businessNetworkConnection.connection = mockConnection;
+ });
+
+ it('should throw if participant not specified', () => {
+ (() => {
+ businessNetworkConnection.bindIdentity(null, pem);
+ }).should.throw(/participant not specified/);
+ });
+
+ it('should throw if certificate not specified', () => {
+ (() => {
+ let mockResource = sinon.createStubInstance(Resource);
+ mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1');
+ businessNetworkConnection.bindIdentity(mockResource, null);
+ }).should.throw(/certificate not specified/);
+ });
+
+ it('should submit a request to the chaincode for a resource', () => {
+ sandbox.stub(Util, 'invokeChainCode').resolves();
+ let mockResource = sinon.createStubInstance(Resource);
+ mockResource.getFullyQualifiedIdentifier.returns('org.doge.Doge#DOGE_1');
+ return businessNetworkConnection.bindIdentity(mockResource, pem)
+ .then(() => {
+ sinon.assert.calledOnce(Util.invokeChainCode);
+ sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', pem]);
+ });
+ });
+
+ it('should submit a request to the chaincode for a fully qualified identifier', () => {
+ sandbox.stub(Util, 'invokeChainCode').resolves();
+ return businessNetworkConnection.bindIdentity('org.doge.Doge#DOGE_1', pem)
+ .then(() => {
+ sinon.assert.calledOnce(Util.invokeChainCode);
+ sinon.assert.calledWith(Util.invokeChainCode, mockSecurityContext, 'bindIdentity', ['org.doge.Doge#DOGE_1', pem]);
+ });
+ });
+
});
describe('#bindIdentity', () => {
@@ -1076,4 +1138,37 @@ describe('BusinessNetworkConnection', () => {
});
+ describe('#getIdentityRegistry', () => {
+
+ it('should perform a security check', () => {
+
+ // Set up the mock.
+ let stub = sandbox. stub(Util, 'securityCheck');
+ let identityRegistry = sinon.createStubInstance(IdentityRegistry);
+ identityRegistry.id = 'org.hyperledger.composer.system.Identity';
+ identityRegistry.name = 'such registry';
+ sandbox.stub(Registry, 'getAllRegistries').resolves([identityRegistry]);
+
+ // Invoke the function.
+ return businessNetworkConnection
+ .getIdentityRegistry()
+ .then(() => {
+ sinon.assert.calledTwice(stub);
+ });
+
+ });
+
+ it('should throw when the default identity registry does not exist', () => {
+
+ // Set up the mock.
+ sandbox.stub(IdentityRegistry, 'getIdentityRegistry').resolves(null);
+
+ // Invoke the function.
+ return businessNetworkConnection
+ .getIdentityRegistry()
+ .should.be.rejectedWith(/default identity registry/);
+
+ });
+ });
+
});
diff --git a/packages/composer-client/test/identityregistry.js b/packages/composer-client/test/identityregistry.js
new file mode 100644
index 0000000000..8c67bab26b
--- /dev/null
+++ b/packages/composer-client/test/identityregistry.js
@@ -0,0 +1,193 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const Factory = require('composer-common').Factory;
+const ModelManager = require('composer-common').ModelManager;
+const IdentityRegistry = require('../lib/identityregistry');
+const Registry = require('../lib/registry');
+const SecurityContext = require('composer-common').SecurityContext;
+const Serializer = require('composer-common').Serializer;
+const Util = require('composer-common').Util;
+
+const chai = require('chai');
+chai.should();
+chai.use(require('chai-things'));
+const sinon = require('sinon');
+
+describe('IdentityRegistry', () => {
+
+ let sandbox;
+ let mockSecurityContext;
+ let mockModelManager;
+ let mockFactory;
+ let mockSerializer;
+ let registry;
+
+ beforeEach(() => {
+ sandbox = sinon.sandbox.create();
+ mockSecurityContext = sinon.createStubInstance(SecurityContext);
+ mockModelManager = sinon.createStubInstance(ModelManager);
+ mockFactory = sinon.createStubInstance(Factory);
+ mockSerializer = sinon.createStubInstance(Serializer);
+ registry = new IdentityRegistry('org.hyperledger.composer.system.Identity', 'wowsuchregistry', mockSecurityContext, mockModelManager, mockFactory, mockSerializer);
+ sandbox.stub(Util, 'securityCheck');
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ });
+
+ describe('#getIdentityRegistry', () => {
+
+ it('should throw when modelManager not specified', () => {
+ (function () {
+ IdentityRegistry.getIdentityRegistry(mockSecurityContext, null, mockFactory, mockSerializer);
+ }).should.throw(/modelManager not specified/);
+ });
+
+ it('should throw when factory not specified', () => {
+ (function () {
+ IdentityRegistry.getIdentityRegistry(mockSecurityContext, mockModelManager, null, mockSerializer);
+ }).should.throw(/factory not specified/);
+ });
+
+ it('should throw when serializer not specified', () => {
+ (function () {
+ IdentityRegistry.getIdentityRegistry(mockSecurityContext, mockModelManager, mockFactory, null);
+ }).should.throw(/serializer not specified/);
+ });
+
+ it('should invoke the chain-code and return the transaction registry', () => {
+
+ // Set up the responses from the chain-code.
+ sandbox.stub(Registry, 'getAllRegistries', () => {
+ return Promise.resolve(
+ [{id: 'org.hyperledger.composer.system.Identity', name: 'doge registry'}]
+ );
+ });
+
+ // Invoke the getAllTransactionRegistries function.
+ return IdentityRegistry
+ .getIdentityRegistry(mockSecurityContext, mockModelManager, mockFactory, mockSerializer)
+ .then((identityRegistry) => {
+
+ // Check that the registry was requested correctly.
+ sinon.assert.calledWith(Util.securityCheck, mockSecurityContext);
+ sinon.assert.calledOnce(Registry.getAllRegistries);
+ sinon.assert.calledWith(Registry.getAllRegistries, mockSecurityContext, 'Asset');
+
+ // Check that the transaction registries were returned correctly.
+ identityRegistry.should.be.an.instanceOf(IdentityRegistry);
+ identityRegistry.id.should.equal('org.hyperledger.composer.system.Identity');
+ identityRegistry.name.should.equal('doge registry');
+
+ });
+
+ });
+
+ it('should handle an error from the chain-code', () => {
+
+ // Set up the responses from the chain-code.
+ sandbox.stub(Registry, 'getAllRegistries', () => {
+ return Promise.reject(
+ new Error('failed to invoke chain-code')
+ );
+ });
+
+ // Invoke the getAllTransactionRegistries function.
+ return IdentityRegistry
+ .getIdentityRegistry(mockSecurityContext, mockModelManager, mockFactory, mockSerializer)
+ .then((identityRegistry) => {
+ throw new Error('should not get here');
+ }).catch((error) => {
+ error.should.match(/failed to invoke chain-code/);
+ });
+
+ });
+
+ it('should not throw if Identity registry is not found', () => {
+ sandbox.stub(Registry, 'getAllRegistries', () => {
+ return Promise.resolve(
+ [{id: 'org.hyperledger.composer.system.Doge', name: 'doge registry'}]
+ );
+ });
+ (function () {
+ IdentityRegistry.getIdentityRegistry(mockSecurityContext, mockModelManager, mockFactory, mockSerializer);
+ }).should.not.throw();
+ });
+ });
+
+ describe('#add', () => {
+
+ it('should throw an unsupported operation when called', () => {
+ (() => {
+ registry.add(null);
+ }).should.throw(/cannot add identity to an identity registry/);
+ });
+
+ });
+
+ describe('#addAll', () => {
+
+ it('should throw an unsupported operation when called', () => {
+ (() => {
+ registry.addAll(null);
+ }).should.throw(/cannot add identities to a identity registry/);
+ });
+
+ });
+
+ describe('#update', () => {
+
+ it('should throw an unsupported operation when called', () => {
+ (() => {
+ registry.update(null);
+ }).should.throw(/cannot update identities in an identity registry/);
+ });
+
+ });
+
+ describe('#updateAll', () => {
+
+ it('should throw an unsupported operation when called', () => {
+ (() => {
+ registry.updateAll(null);
+ }).should.throw(/cannot update identities in an identity registry/);
+ });
+
+ });
+
+ describe('#remove', () => {
+
+ it('should throw an unsupported operation when called', () => {
+ (() => {
+ registry.remove('dogecar1');
+ }).should.throw(/cannot remove identities from an identity registry/);
+ });
+
+ });
+
+ describe('#removeAll', () => {
+
+ it('should throw an unsupported operation when called', () => {
+ (() => {
+ registry.removeAll(null);
+ }).should.throw(/cannot remove identities from an identity registry/);
+ });
+
+ });
+
+});
diff --git a/packages/composer-common/lib/introspect/classdeclaration.js b/packages/composer-common/lib/introspect/classdeclaration.js
index fa80673402..da89dbf9a1 100644
--- a/packages/composer-common/lib/introspect/classdeclaration.js
+++ b/packages/composer-common/lib/introspect/classdeclaration.js
@@ -167,6 +167,17 @@ class ClassDeclaration {
if(classDecl===null) {
throw new IllegalModelException('Could not find super type ' + this.superType, this.modelFile, this.ast.location);
}
+
+ // Prevent extending declaration with different type of declaration
+ const supertypeDeclaration = this.getModelFile().getType(this.superType);
+ if (supertypeDeclaration) {
+ if (this.constructor.name !== supertypeDeclaration.constructor.name) {
+ let typeName = this.getSystemType();
+ let superTypeName = supertypeDeclaration.getSystemType();
+ throw new IllegalModelException(`${typeName} (${this.getName()}) cannot extend ${superTypeName} (${supertypeDeclaration.getName()})`, this.modelFile, this.ast.location);
+ }
+ }
+
// TODO (DCS)
// else {
// // check that assets only inherit from assets etc.
diff --git a/packages/composer-common/lib/introspect/eventdeclaration.js b/packages/composer-common/lib/introspect/eventdeclaration.js
index e1162227bb..edef12cf0b 100644
--- a/packages/composer-common/lib/introspect/eventdeclaration.js
+++ b/packages/composer-common/lib/introspect/eventdeclaration.js
@@ -16,6 +16,7 @@
const ClassDeclaration = require('./classdeclaration');
const IllegalModelException = require('./illegalmodelexception');
+const ModelUtil = require('../modelutil');
/** Class representing the definition of an Event.
* @extends ClassDeclaration
@@ -58,6 +59,19 @@ class EventDeclaration extends ClassDeclaration {
if(!this.isSystemType() && this.getName() === 'Event') {
throw new IllegalModelException('Event is a reserved type name.', this.modelFile, this.ast.location);
}
+
+ let systemTypeDeclared = true;
+
+ // If using models without importing system models
+ try {
+ this.getModelFile().getType(ModelUtil.getSystemNamespace() + '.' + this.getSystemType());
+ } catch (e) {
+ systemTypeDeclared = false;
+ }
+
+ if (!this.isSystemType() && this.idField && systemTypeDeclared) {
+ throw new IllegalModelException('Event should not specify an identifying field.', this.modelFile, this.ast.location);
+ }
}
/**
diff --git a/packages/composer-common/lib/introspect/transactiondeclaration.js b/packages/composer-common/lib/introspect/transactiondeclaration.js
index 4c807dfcb0..06485b78c3 100644
--- a/packages/composer-common/lib/introspect/transactiondeclaration.js
+++ b/packages/composer-common/lib/introspect/transactiondeclaration.js
@@ -16,6 +16,8 @@
const ClassDeclaration = require('./classdeclaration');
const IllegalModelException = require('./illegalmodelexception');
+const ModelUtil = require('../modelutil');
+
/** Class representing the definition of an Transaction.
* @extends ClassDeclaration
@@ -58,6 +60,19 @@ class TransactionDeclaration extends ClassDeclaration {
if(!this.isSystemType() && this.getName() === 'Transaction') {
throw new IllegalModelException('Transaction is a reserved type name.', this.modelFile, this.ast.location);
}
+
+ let systemTypeDeclared = true;
+
+ // If using models without importing system models
+ try {
+ this.getModelFile().getType(ModelUtil.getSystemNamespace() + '.' + this.getSystemType());
+ } catch (e) {
+ systemTypeDeclared = false;
+ }
+
+ if (!this.isSystemType() && this.idField && systemTypeDeclared) {
+ throw new IllegalModelException('Transaction should not specify an identifying field.', this.modelFile, this.ast.location);
+ }
}
}
diff --git a/packages/composer-common/lib/system.cto b/packages/composer-common/lib/system.cto
index 84618d4c98..ec816a76fb 100644
--- a/packages/composer-common/lib/system.cto
+++ b/packages/composer-common/lib/system.cto
@@ -4,7 +4,7 @@ abstract asset Asset { }
abstract participant Participant { }
-abstract transaction Transaction identified by transactionId{
+abstract transaction Transaction {
o String transactionId
o DateTime timestamp
}
diff --git a/packages/composer-common/test/data/model/concept2.cto b/packages/composer-common/test/data/model/concept2.cto
index a6b1ec85f8..8a568c395d 100755
--- a/packages/composer-common/test/data/model/concept2.cto
+++ b/packages/composer-common/test/data/model/concept2.cto
@@ -146,18 +146,18 @@ asset POContractorRecord identified by poContractorKey {
*****************************/
/** Create purchase order from PO provider, ie SAP system */
-transaction CreatePO identified by trxId {
+transaction CreatePO {
o String trxId
o PurchaseOrder po
}
-transaction UpdatePOContractorSupplierDetails identified by trxId {
+transaction UpdatePOContractorSupplierDetails {
o String trxId
o String poNumber
o POContractorSupplierDetails poContractorSupplierDetails
}
-transaction UpdatePOContractorBuyerDetails identified by trxId {
+transaction UpdatePOContractorBuyerDetails {
o String trxId
o String poNumber
o POContractorBuyerDetails poContractorBuyerDetails
@@ -166,7 +166,7 @@ transaction UpdatePOContractorBuyerDetails identified by trxId {
/**
* Transaction for supplier account manager to approve purchase order buyer details
*/
-transaction ApprovePOContractorBuyerDetails identified by trxId {
+transaction ApprovePOContractorBuyerDetails {
o String trxId
o String poNumber
}
@@ -174,7 +174,7 @@ transaction ApprovePOContractorBuyerDetails identified by trxId {
/**
* Transaction for buyer manager to approve purchase order supplier details
*/
-transaction ApprovePOContractorSupplierDetails identified by trxId {
+transaction ApprovePOContractorSupplierDetails {
o String trxId
o String poNumber
}
diff --git a/packages/composer-common/test/data/model/dependencies/contract/proforma.cto b/packages/composer-common/test/data/model/dependencies/contract/proforma.cto
index 183aedf18b..0bff5acfa3 100644
--- a/packages/composer-common/test/data/model/dependencies/contract/proforma.cto
+++ b/packages/composer-common/test/data/model/dependencies/contract/proforma.cto
@@ -41,13 +41,13 @@ asset ProForma identified by id {
o Clause[] clauses
}
-transaction CreateProForma identified by id {
+transaction CreateProForma {
o String id
o String description
o Clause[] clauses
}
-transaction UpdateProForma identified by id {
+transaction UpdateProForma {
o String id
o String description
o Clause[] clauses
diff --git a/packages/composer-common/test/data/model/farm2fork.cto b/packages/composer-common/test/data/model/farm2fork.cto
index 8aa35247d2..d0ceba4d77 100644
--- a/packages/composer-common/test/data/model/farm2fork.cto
+++ b/packages/composer-common/test/data/model/farm2fork.cto
@@ -57,7 +57,7 @@ abstract transaction AnimalTransaction {
}
-transaction Foo extends Meat {
+transaction Foo {
}
diff --git a/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto b/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto
index 901e107513..82c4d9e821 100644
--- a/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto
+++ b/packages/composer-common/test/data/parser/classdeclaration.dupetransactionname.cto
@@ -1,11 +1,11 @@
namespace com.testing
-transaction AssetTransaction identified by id {
+transaction AssetTransaction {
o String id
o String newValue
}
-transaction AssetTransaction identified by id {
+transaction AssetTransaction {
o String id
o String newValue
}
\ No newline at end of file
diff --git a/packages/composer-common/test/introspect/functiondeclaration.js b/packages/composer-common/test/introspect/functiondeclaration.js
index 52f6cbd9f3..781bf3a4c6 100644
--- a/packages/composer-common/test/introspect/functiondeclaration.js
+++ b/packages/composer-common/test/introspect/functiondeclaration.js
@@ -25,7 +25,7 @@ const sinon = require('sinon');
describe('FunctionDeclaration', () => {
const modelManager = new ModelManager();
- modelManager.addModelFile('namespace org.acme transaction TestTransaction identified by id {o String id}');
+ modelManager.addModelFile('namespace org.acme transaction TestTransaction {}');
let mozartModel = fs.readFileSync('test/data/model/mozart.cto', 'utf8');
modelManager.addModelFile(mozartModel, 'mozart.cto');
diff --git a/packages/composer-common/test/introspect/transactiondeclaration.js b/packages/composer-common/test/introspect/transactiondeclaration.js
index e384e121f5..952d897260 100644
--- a/packages/composer-common/test/introspect/transactiondeclaration.js
+++ b/packages/composer-common/test/introspect/transactiondeclaration.js
@@ -23,20 +23,17 @@ require('chai').should();
const sinon = require('sinon');
describe('TransactionDeclaration', () => {
-
- let mockModelManager;
let mockSystemTransaction;
let mockClassDeclaration;
let sandbox;
+ let modelManager;
beforeEach(() => {
sandbox = sinon.sandbox.create();
- mockModelManager = sinon.createStubInstance(ModelManager);
+ modelManager = new ModelManager();
mockSystemTransaction = sinon.createStubInstance(TransactionDeclaration);
mockSystemTransaction.getFullyQualifiedName.returns('org.hyperledger.composer.system.Transaction');
- mockModelManager.getSystemTypes.returns([mockSystemTransaction]);
mockClassDeclaration = sinon.createStubInstance(ClassDeclaration);
- mockModelManager.getType.returns(mockClassDeclaration);
mockClassDeclaration.getProperties.returns([]);
});
@@ -45,15 +42,15 @@ describe('TransactionDeclaration', () => {
});
describe('#validate', () => {
+
it('should throw error name is Transaction', () => {
const model = `
namespace com.test
- transaction Transaction identified by transactionId {
- o String transactionId
+ transaction Transaction {
}`;
- const modelFile = new ModelFile(mockModelManager, model);
+ const modelFile = new ModelFile(modelManager, model);
const p = modelFile.getTransactionDeclarations()[0];
(() => {
diff --git a/packages/composer-playground/e2e/data/bna/basic-sample-network.bna b/packages/composer-playground/e2e/data/bna/basic-sample-network.bna
index 5bce114bb238f88a395565cae82bfa4162e565d0..2404572cb450a010dac2c295bc6d44aeb87fc380 100644
GIT binary patch
literal 6444
zcmb_gPmdc(75B3LgaN@yNN{{LxB+cVkCqMaoHCz%kF>`XM{Y_tk2Q|>O0JJas&
zt*Rbp6b%P1NL)B@mOzL3oRS@NiBG*y{d%>4G
z;V0ovaM=$sv5;7p3Y|m)Efz&82bna>T%85|;9M%5t3UwF5
zc@z9$9l$kI>5yeW{+YZ0%3?s_dEoE?$u*Pra|OrW^aD|vdCn$nLK%3fIDlY;G?>f5
z%?h;V_hlioSY}b;l3}XyC30bgW-di6RcKx^nx|gqqK~X70&_Nm3k03L!l~_)e28eA^hOm#ccDU*m4(QyjdssabOGwM}@&`9<&;=jAI13Q$+Kw3Y}$}HkCT|c9U
zReP70Pg))1Uw{1l&t~s-I{f`MI>^!f=>BK>;UfMCJ(DR?Qj*Cx7WQl1?qr^5!asvn
z5O}>HcvCY7D_)oT6w4VaNP?!N=3knsK2)~?+g=1=^E8ztw3ddrEK%>|*fy>aFP&`n
zA)_KHghFvm3e?y(>62!o#bDq=Ev(<#43lQVGGuXciabXY+T{DZbg~T_eckj4x|yVW
znaP-@7m!fx`szS^2xoVmi?o!4;)9NAE-23$24^ZoBBQA!WI-}ZoJ?I#7l|=&QbX-4
z9}+k1>Rf+=B>33>)t)l}iU>nlZukJ~76sB)SX^;{SQ|U
z8%{X%Ih`VY05S3)rXKS_wS3O2ZFx%0Fc)!7teoY`b;4Ndxme9DF>L;#G6WJc
z9LsTPB>17|(?W1XTGP!zeuW254Ism%0|(3c%MMF1f}qj_m5xR;lpE_j(;yiNYTyUj
zKqy>?7H7HjJTDPcEj`Cv1tUAg9lH`!1F^<1v{|{R!qbYD;fB!LG9U>>7gG@9rYvIM
z3&uC{m+Y{WAm3A5c^cjEuv9EH1EA={RTlIR2HgET+cJCr!_I=b##Mr`SQ$VSI1uqU
zg9x_Uih|0Py{7aLS1G6D*a{dP$s!ezMEx=*&v4{J4nN8;r3gdD)SK{qtzC=~vc=p-MS&>&=!p9rt_b3=)j$ab`4i!yx)C43UzuiF$dF-RN*%@31R*XjuG4L=H
zUC6L~)*h97dZdMNaRGEGM|M3a0nmx;3d;Tu^#g9tCoNIu-H
zY|4fA+mUkao)Bi)!U}qw{KIi
zzPfEsTf6c&-?g=3MNxMY!h8Syz{$%zt!pR_k)J00yxI?c2-^)VC-yTEpXY%
zmu`&<3VLTdS-M?UP@OBS3kZGDndwFbuJhyAVmhZ|qd1!_(>R8;gTbAuHh2kV2M&eZ
z6@xA2oIYOaZs2Pl?a{6)(Ax43nk7mbzM1=rsi@YVfR|>K0v`0Om5oWxAJ$l39OjPC
zc~W9~q#eba`)tGc-$Y=9aUwh)d1BjWSm6af-oZCbrfYZ^74QA!9~h?j`@M>y0?P~B
zRk(mZc5e(~Dsd}yjoDpU>zzJ*?q0qSZdBCcW*AgV!S<&tg8SD(T<~y)X2h);{co0_
z@Z)A83&TB-@6&0*Esd7ArrVxcNnt1wk0dN3pBe#j0fqukZm_EeNf?kaHQ)Gg;a@J7
zp|JPs5cnOY&J;a-JifPoaJ6-y)liBl!9htdlT
zxPJrEebV{NEHN$fx%^H@CFrXlYy4HK2djMo9+NY4vtt?`2XuFIJU;HTkZ0q`!^5W&
zdNw*b8XZi=`^R*6ME4F4?vE$q!vj1$pwYn>^k{r=zfTh0LR{oayefhPP!nSUMk&lI
zkacV)BU#7F&Mh1vUC0Q7as<<|(-QYpI^|oN?fqUO=ZTcXqzPuAYM8!amn#+Bc4_g`
zrJ8%7T7A{Cy8QylC>Hc;t+a$LDZK2uB=gc<1{^fWmt9ZOPNKI1wTIpMylP7FWcJL<
zZKNF1lV~_ZMq>hE$M7A&O^_9~Y!*X0KIcO5zD+>74W&Z2Y3V;*$$H9jp_69!8$hov
z9!_3cfRk7!b0hATb_}d1SS3#xV;8GPdivpAVW~2{u8g?&9#y2ht<4^`L|d-)uz4Zu
z&w4AD^{nJt$jVtv$saiFilImg_Mv4&E;&t|t&WMXYISVM4(%i@5fbN6R1NEI=DB>m
zew$R}p{fl_NPqbgluXC=y
zfN41I;-&uTb<8^3`cHN9ks7`yKpFDaO&SGQU@B9ga+8=(zPyYpt
C=LI$Z
literal 5574
zcmb7|3pCUJ|HsFMk^7~PTe+mLCQ{@!a%W*KQ)x5CT!!3ArE*&rG532Y_sFF$$}M-f
z6D1<|$|biF67}2i{c8EA&VM_*owGgmdcW?^kD(qA!~h`QM$W$1{qg4CCoTXh0AuBD
zZ!P6+g?DwrN;zXaF1WZk9yUcX0w!!w7Imb@XDUvBAql|=0F&<95du3QASV$hgole8
zmdqk^{$tI?6vFGEoErH(;PgZ6JfY~eCX-unq9#+-)zm5g{2>!?0J|r;wf5P9kG}o_
zAIfm7w%UYYUVAya`@*bQ*q9=~wy2H8ms=I|K81Mr)^RiNq!dqMo)P?jhWw~oix_&|
zl7;47Y$E5~uNOqdM;`h{Jr)nM7JC_G{`JF4=W&6U*fGs_5;~0MgLS(vyh-f;xhiuV-NZlv6o(l)u`&`#nTlLCpW`1G~ob==RITn`Df43$3a
z40=qwFW!=2op`_I(lx?;@haA@SF|T1^X@WLu}l_3-I@x3PrdFhfTXA^+_zS^hbZ%U
zr`}rd1v=PYpf2sCq_38#vetV0qsydx!}7Y|wc+m)w?E#Hw(?2}Ug&4dMR&eF+1Q{3
zc(aG<+%5}8;@C#-XEM+N1>(e7Rsaq4=)edSy$ww&006`d_z|JaATMkSGCe6mD3k$A
z{S*SZ6;v
z4&??OVBJeqxR=#`l}oh^V7{;VI`B9l00fKUf?7_Jy+H?F;(1jeOG@;Mqz5^FH;;)!
zkN;8vJUivDlRak3TOJ~(x4gA^!tI#-f5L1Q7sSDxTnc2?e>_hUBbV1u)~oN$6EE&F
zk&Ih@^1%3?Xy@mLLcvATsX8Gqj4^g4>Z+#SzjnZ>o6%RPzCQgrSKj#^bWHe(uU%3~
zSFD<-u?c#Jtu|bAsYlax!B5xy;7G!EF2!BkkMAum^&0bGX=qOPh0-1huCF=z;bO@3
z8z4R4S=IePN8am;fR7%V&x5y)oe6myt?tYud^|NE+^bxfHFjT?M%7-#RTjUpJfC;<
zYsOgEP_@SbpHkbN#r0kE&Kz?4wI0GolW$8ncwGjWg{K7Ib!bNcO^r{g0XQ&&rl0IL+dUw8_thDcM5LkZ#nbwp^$Se&SLaBo#~!qEysj_DJm
z)+g?0iHk^xK&ofa*{(VI+J?RBA-jn(s%}BkBlk>7fZ@##BVrW>F!jCxcb!D0*g2k#
z(-(_9x{U?L4W_ccV2jLR$eRszV2?c=pSWibIvxU9dlcyRT>Qy-c;lTGgi%9)?JWae
zrnW7lYtbV@$%nxp9b|)3yZprA-q1u@9kXm)nMwJT
z`y4~fM~I(w;I^JKr&E_CAyI4oMP}pI2lDyCUVV|SREk+!uX^ej{!TV?*>qGOP~O|5
zDUxtE>1=1NbH5(G>q2ko0bUlJB!gLlw%IR%J`p_nymXdVFe+}9Z=2#ZFR8mR^(3Uv
z%^yV`WB<$4>9P&oMSXAvanyDn6OJ
z*mkio=f109HFNn>(c?)`fhMpf0&7(bqOP_P_8_dp&5z%ncocRslVK+0kb&`g!zd@Y
zHEo&8?Bj&yx<$I~Z>@DNsX%=eHu6fGuOAcXV^Gda(!TyGAG<%#nC!9Mz^UPE^
zdn=49(;;&|YSbCwWqR9dcH22IqS-VGziqVhp_BK$(ny~Y%yx1D3atH|(eEUYq3ywLqD&CSzkNmi37Y4#3goGJJ8C*GNSXEtQmIcgeZp
z)`aDZN}S7K^PDpv@WNoTG-uZaV1haF(L{`=F6%%<`34y7+jm_Ul?l==eU^G{v12Nj
zxv}ry6y2S)$7*F^U7Fg0P5nG^5eM29VRP4A=_CQ!jxMR=uEZ7AC3*3y7^{Hrcmr5G
zLbSUB+D4tvJOUhY)HFCNds2s^D^z8M;fexld`j;Pov(zqSNd!{lyiHi?%arHseVkC
z*?hd5z7+vnykMzxX&o~y47T;t*P>HYAkw&nKa4!nld7&;6x}!>@6bx8Ij#EIUu)W6
z+I+(3!pO9i%8B#kt#I4U=EQoVB0mM)g(kwunknRAf6uS7xz}ekCp9x58kq}<0njzi
zo)qVs{24h3qxjW!#cCFg1=)t`)iZsmlA#PNroQM2Aa*!}&gkR9BAwSs4_u!(j?0GH?_h
zgohL9rP{3;rZ33lO?;OM)$#1cod@%sK>Y^nLFfUs+oIuM@Tn4;{yqD
zm+6{Z+@oST+-krZenGKte7!dKhltNG;8(I{#II_Eh<0a(3@^Q^z*Gt3WL4XTrM+&8Pzkk02{kA*xhbK
z5oOHmU%c;(e__KPu6^Ke{F-tjd~jFf5l{NK)6@M<-@+^H-rsM%T8tPBhbnV@b+qwM
zAp%CIM&;srM*8Kh-4g3Se;clUf7yRYd|f@?lRr7Dcy>3}%OPPD_9^qt8ktXF)cY|V
zOYY|DGqkk}s3rHcI1VQEeOKn;qH6FcB?a@n6Pe6RImK!%yJGdlMxJT~>6x$m2!)iX
zX7xc*cT)pWhT@~~ubM*6z)Vy}nqeY=d0%K2?y{bz8OFQJI`Z2ae1{o+EW7z$8<`%f!b_tU0
zf8LoPs6QNI=S_E!SBQ3TUYeDLPhHI~ML#rzNA&|CBYvWHYMe!7xG#h5mA7ByP7spe}0BazCL<&
zb#+KarB1?|;{j>
z|KZMzbVd1^B7YmhajPa=Ujy`TpFUl>P+#`VkR^fdnWXPE-FZCtaW|B$9uaEgWd}p1
zAZkoIgx6rU+D0cQWWec;Fn%sZ+CDaq=VgtiZHgZho=LW43_PANFJPMt1PW!hB8>Q9
zH-*%1<}ytb%~#4~(OAqH#y=lWlkdtl?&zuq1!UZfZNHfFnQeItWgB-k#ImWQ+wEM(
zd2xT{81gIVPYD;?Qe}#Bwi+wadQO$zwZg
zq<(PbSRoEggXonKu%EEQzN;D+^5fP_j#j!9z|1l|l6WR~J!*g{*!2q^FV
z2}hw|!CO~6Ho2Q-{$FMO)65OnTT*M>Y96M@P1DHQ`cDW-(>7pyB;RZy{&?h{+@D9L
z6wC*IIWq3A++Xzj?@zzIW$;k^#Via+8VbL)cvFXOdV{Q+DFa1DQi_)Slt^FQKOreJ
zIe1HxQ+j&4xqOst*iq6t@mq7ZM9|I_Q*QcVlvLkqf1y$eqU~1yye>hx^6dMC|A=k}
ztAAAwJEJK_xX3SP`dvGrw{K9tr;|r1<&YKng&s*Nl;67hmuU*#S_&wWgiNL!ViFYO
zG+#rezm?3kk^TEWA!iF^ccQGzSzle|9n7bgj+AbpA>{!8z)ZTo1_1!QtmIGs1BtR%
A*8l(j
diff --git a/packages/composer-playground/e2e/tests/editor-define.spec.ts b/packages/composer-playground/e2e/tests/editor-define.spec.ts
index 94c89d219c..3687e6a275 100644
--- a/packages/composer-playground/e2e/tests/editor-define.spec.ts
+++ b/packages/composer-playground/e2e/tests/editor-define.spec.ts
@@ -1,4 +1,4 @@
-import { browser, element, by } from 'protractor';
+import { browser, element, by } from 'protractor';
import { ExpectedConditions } from 'protractor';
import { OperationsHelper } from '../utils/operations-helper.ts';
import { EditorHelper } from '../utils/editor-helper.ts';
@@ -16,12 +16,14 @@ let should = chai.should();
describe('Editor Define', (() => {
- // Navigate to Editor base page and move past welcome splash
- beforeAll(() => {
- OperationsHelper.navigatePastWelcome();
- });
-
describe('On initialise', (() => {
+
+ // Navigate to Editor base page and move past welcome splash
+ beforeAll(() => {
+ browser.get(browser.baseUrl);
+ OperationsHelper.navigatePastWelcome();
+ });
+
it('should initialise the side navigation with basic sample network', (() => {
// Check Files (Basic Sample Files) Present and clickable
// -within
@@ -59,6 +61,11 @@ describe('Editor Define', (() => {
}));
describe('Import BND button', (() => {
+ // Navigate to Editor base page and move past welcome splash
+ beforeAll(() => {
+ browser.get(browser.baseUrl);
+ OperationsHelper.navigatePastWelcome();
+ });
// Press the 'Import' button
beforeEach(() => {
@@ -147,6 +154,12 @@ describe('Editor Define', (() => {
describe('Export BND button', (() => {
+ // Navigate to Editor base page and move past welcome splash
+ beforeAll(() => {
+ browser.get(browser.baseUrl);
+ OperationsHelper.navigatePastWelcome();
+ });
+
it('should export BNA named as the package name', (() => {
let filename;
EditorHelper.retrieveDeployedPackageName()
@@ -191,11 +204,32 @@ describe('Editor Define', (() => {
describe('Add File button', (() => {
- // Press the 'AddFile' button
+ let startFiles = [ 'About\nREADME.md',
+ 'Model File\nmodels/sample.cto',
+ 'Script File\nlib/sample.js',
+ 'Access Control\npermissions.acl'];
+
+ // Navigate to Editor base page and move past welcome splash
+ beforeAll(() => {
+ browser.get(browser.baseUrl);
+ OperationsHelper.navigatePastWelcome();
+ });
+
+ // Reset the BNA and then press the 'AddFile' button
beforeEach(() => {
- EditorHelper.addFile()
+
+ EditorHelper.importBND()
.then(() => {
- AddFileModalHelper.waitForAddFileModalToAppear();
+ ImportModalHelper.waitForImportModalToAppear();
+ ImportModalHelper.selectBusinessNetworkDefinitionFromFile('./e2e/data/bna/basic-sample-network.bna');
+ ReplaceModalHelper.confirmReplace();
+ browser.wait(ExpectedConditions.invisibilityOf(element(by.css('.import'))), 10000);
+ browser.wait(ExpectedConditions.invisibilityOf(element(by.id('success_notify'))), 10000);
+
+ EditorHelper.addFile()
+ .then(() => {
+ AddFileModalHelper.waitForAddFileModalToAppear();
+ });
});
});
@@ -266,310 +300,252 @@ describe('Editor Define', (() => {
}));
it('should enable the addition of a script file via radio button selection', (() => {
- let startFiles = EditorHelper.retrieveNavigatorFileNames()
- .then((names) => {
- startFiles = names;
- })
- .then(() => {
- AddFileModalHelper.selectScriptRadioOption();
- })
- .then(() => {
- // Add File
- AddFileModalHelper.confirmAdd();
- })
- .then(() => {
- browser.waitForAngularEnabled(false);
- // Check extracted against new template file that we should have in the list
- EditorHelper.retrieveNavigatorFileNames()
- .then((list: any) => {
- // Previous files should still exist
- startFiles.forEach((element) => {
- list.includes(element).should.be.true;
- });
- // We should have added one file
- list.length.should.be.equal(startFiles.length + 1);
- list.includes('Script File\nlib/script.js').should.be.true;
- browser.waitForAngularEnabled(true);
+ AddFileModalHelper.selectScriptRadioOption();
+ AddFileModalHelper.confirmAdd();
+ browser.waitForAngularEnabled(false);
+ // Check extracted against new template file that we should have in the list
+ EditorHelper.retrieveNavigatorFileNames()
+ .then((list: any) => {
+ // Previous files should still exist
+ startFiles.forEach((element) => {
+ list.includes(element).should.be.true;
});
- // -deploy enabled
+ // We should have added one file
+ list.length.should.be.equal(startFiles.length + 1);
+ list.includes('Script File\nlib/script.js').should.be.true;
+ browser.waitForAngularEnabled(true);
+ });
+ // -deploy enabled
+ EditorHelper.retrieveNavigatorFileActionButtons()
+ .then((array: any) => {
+ array[1].enabled.should.be.equal(true);
+ });
+ // -active file
+ EditorHelper.retrieveNavigatorActiveFile()
+ .then((list: any) => {
+ list.length.should.equal(1);
+ list.includes('Script File\nlib/script.js').should.be.true;
+ });
+ // deploy new item
+ EditorHelper.deployBND()
+ .then(() => {
+ // -success message
+ browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
+ // -deploy disabled
EditorHelper.retrieveNavigatorFileActionButtons()
.then((array: any) => {
- array[1].enabled.should.be.equal(true);
- });
- // -active file
- EditorHelper.retrieveNavigatorActiveFile()
- .then((list: any) => {
- list.length.should.equal(1);
- list.includes('Script File\nlib/script.js').should.be.true;
- });
- // deploy new item
- EditorHelper.deployBND()
- .then(() => {
- // -success message
- browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
- // -deploy disabled
- EditorHelper.retrieveNavigatorFileActionButtons()
- .then((array: any) => {
- array[1].enabled.should.be.equal(false);
- });
+ array[1].enabled.should.be.equal(false);
});
});
}));
it('should enable the addition of a script file via file input event', (() => {
- let startFiles;
+ AddFileModalHelper.selectFromFile('./e2e/data/files/importScript.js');
+ browser.waitForAngularEnabled(false);
+ // Check extracted against new file that we should have in the list
EditorHelper.retrieveNavigatorFileNames()
- .then((names) => {
- startFiles = names;
- })
- .then(() => {
- AddFileModalHelper.selectFromFile('./e2e/data/files/importScript.js');
- })
- .then(() => {
- browser.waitForAngularEnabled(false);
- // Check extracted against new file that we should have in the list
- EditorHelper.retrieveNavigatorFileNames()
- .then( (list: any) => {
- // Previous files should still exist
- startFiles.forEach((element) => {
- list.includes(element).should.be.true;
- });
- // We should have added one file
- list.length.should.be.equal(startFiles.length + 1);
- list.includes('Script File\nlib/importScript.js').should.be.true;
- browser.waitForAngularEnabled(true);
+ .then( (list: any) => {
+ // Previous files should still exist
+ startFiles.forEach((element) => {
+ list.includes(element).should.be.true;
});
- // -deploy enabled
+ // We should have added one file
+ list.length.should.be.equal(startFiles.length + 1);
+ list.includes('Script File\nlib/importScript.js').should.be.true;
+ browser.waitForAngularEnabled(true);
+ });
+ // -deploy enabled
+ EditorHelper.retrieveNavigatorFileActionButtons()
+ .then((array: any) => {
+ array[1].enabled.should.be.equal(true);
+ });
+ // -active file
+ EditorHelper.retrieveNavigatorActiveFile()
+ .then((list: any) => {
+ list.length.should.equal(1);
+ list.includes('Script File\nlib/importScript.js').should.be.true;
+ });
+ // deploy new item
+ EditorHelper.deployBND()
+ .then(() => {
+ // -success message
+ browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
+ // -deploy disabled
EditorHelper.retrieveNavigatorFileActionButtons()
.then((array: any) => {
- array[1].enabled.should.be.equal(true);
- });
- // -active file
- EditorHelper.retrieveNavigatorActiveFile()
- .then((list: any) => {
- list.length.should.equal(1);
- list.includes('Script File\nlib/importScript.js').should.be.true;
- });
- // deploy new item
- EditorHelper.deployBND()
- .then(() => {
- // -success message
- browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
- // -deploy disabled
- EditorHelper.retrieveNavigatorFileActionButtons()
- .then((array: any) => {
- array[1].enabled.should.be.equal(false);
- });
+ array[1].enabled.should.be.equal(false);
});
});
}));
it('should enable the addition of a model file via radio button selection', (() => {
- let startFiles;
+ AddFileModalHelper.selectModelRadioOption();
+ AddFileModalHelper.confirmAdd();
+ browser.waitForAngularEnabled(false);
+ // -active file
+ EditorHelper.retrieveNavigatorActiveFile()
+ .then((list: any) => {
+ list.length.should.equal(1);
+ list[0].should.be.equal('Model File\nmodels/org.acme.model.cto');
+ browser.waitForAngularEnabled(true);
+ });
+ // Check extracted against new template file that we should have in the list
EditorHelper.retrieveNavigatorFileNames()
- .then((names) => {
- startFiles = names;
- })
- .then(() => {
- AddFileModalHelper.selectModelRadioOption();
- })
- .then(() => {
- // Add option becomes enabled
- AddFileModalHelper.confirmAdd();
- })
- .then(() => {
- browser.waitForAngularEnabled(false);
- // -active file
- EditorHelper.retrieveNavigatorActiveFile()
- .then((list: any) => {
- list.length.should.equal(1);
- list.includes('Model File\nmodels/org.acme.model.cto').should.be.true;
- browser.waitForAngularEnabled(true);
- });
- // Check extracted against new template file that we should have in the list
- EditorHelper.retrieveNavigatorFileNames()
- .then( (list: any) => {
- // Previous files should still exist
- startFiles.forEach((element) => {
- list.includes(element).should.be.true;
- });
- // We should have added one file
- list.length.should.be.equal(startFiles.length + 1);
- list.includes('Model File\nmodels/org.acme.model.cto').should.be.true;
+ .then( (list: any) => {
+ // Previous files should still exist
+ startFiles.forEach((element) => {
+ list.includes(element).should.be.true;
});
- // -deploy enabled
+ // We should have added one file
+ list.length.should.be.equal(startFiles.length + 1);
+ list.includes('Model File\nmodels/org.acme.model.cto').should.be.true;
+ });
+ // -deploy enabled
+ EditorHelper.retrieveNavigatorFileActionButtons()
+ .then((array: any) => {
+ array[1].enabled.should.be.equal(true);
+ });
+ // deploy new item
+ EditorHelper.deployBND()
+ .then(() => {
+ // -success message
+ browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
+ // -deploy disabled
EditorHelper.retrieveNavigatorFileActionButtons()
.then((array: any) => {
- array[1].enabled.should.be.equal(true);
- });
- // deploy new item
- EditorHelper.deployBND()
- .then(() => {
- // -success message
- browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
- // -deploy disabled
- EditorHelper.retrieveNavigatorFileActionButtons()
- .then((array: any) => {
- array[1].enabled.should.be.equal(false);
- });
+ array[1].enabled.should.be.equal(false);
});
});
}));
it('should enable the addition of a model file via file input event', (() => {
- let startFiles;
+
+ AddFileModalHelper.selectFromFile('./e2e/data/files/importModel.cto');
+
+ browser.waitForAngularEnabled(false);
+ // -active file
+ EditorHelper.retrieveNavigatorActiveFile()
+ .then((list: any) => {
+ list.length.should.equal(1);
+ list.includes('Model File\nmodels/importModel.cto').should.be.true;
+ browser.waitForAngularEnabled(true);
+ });
+ // Check extracted against new file that we should have in the list
EditorHelper.retrieveNavigatorFileNames()
- .then((names) => {
- startFiles = names;
- })
- .then(() => {
- AddFileModalHelper.selectFromFile('./e2e/data/files/importModel.cto');
- })
- .then(() => {
- browser.waitForAngularEnabled(false);
- // -active file
- EditorHelper.retrieveNavigatorActiveFile()
- .then((list: any) => {
- list.length.should.equal(1);
- list.includes('Model File\nmodels/importModel.cto').should.be.true;
- browser.waitForAngularEnabled(true);
- });
- // Check extracted against new file that we should have in the list
- EditorHelper.retrieveNavigatorFileNames()
- .then( (list: any) => {
- // Previous files should still exist
- startFiles.forEach((element) => {
- list.includes(element).should.be.true;
- });
- // We should have added one file
- list.length.should.be.equal(startFiles.length + 1);
- list.includes('Model File\nmodels/importModel.cto').should.be.true;
+ .then( (list: any) => {
+ // Previous files should still exist
+ startFiles.forEach((element) => {
+ list.includes(element).should.be.true;
});
- // -deploy enabled
+ // We should have added one file
+ list.length.should.be.equal(startFiles.length + 1);
+ list.includes('Model File\nmodels/importModel.cto').should.be.true;
+ });
+ // -deploy enabled
+ EditorHelper.retrieveNavigatorFileActionButtons()
+ .then((array: any) => {
+ array[1].enabled.should.be.equal(true);
+ });
+ // deploy new item
+ EditorHelper.deployBND()
+ .then(() => {
+ // -success message
+ browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
+ // -deploy disabled
EditorHelper.retrieveNavigatorFileActionButtons()
.then((array: any) => {
- array[1].enabled.should.be.equal(true);
- });
- // deploy new item
- EditorHelper.deployBND()
- .then(() => {
- // -success message
- browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
- // -deploy disabled
- EditorHelper.retrieveNavigatorFileActionButtons()
- .then((array: any) => {
- array[1].enabled.should.be.equal(false);
- });
+ array[1].enabled.should.be.equal(false);
});
});
}));
it('should enable the addition of an ACL file via file input event', (() => {
- let startFiles;
- EditorHelper.retrieveNavigatorFileNames()
- .then((names) => {
- startFiles = names;
- })
- .then(() => {
- AddFileModalHelper.selectFromFile('./e2e/data/files/importACL.acl');
- })
- .then(() => {
- // Replace confirm modal should show
- ReplaceModalHelper.confirmReplace();
- })
- .then(() => {
- browser.waitForAngularEnabled(false);
- // -active file
- EditorHelper.retrieveNavigatorActiveFile()
- .then((list: any) => {
- list.length.should.equal(1);
- list.includes('Access Control\npermissions.acl').should.be.true;
- });
- // Check code mirror for new contents (we only have one permissions file)
- EditorFileHelper.retrieveEditorCodeMirrorText()
- .then((text) => {
- text.should.contain('description: "Newly imported ACL file"');
- });
+
+ AddFileModalHelper.selectFromFile('./e2e/data/files/importACL.acl');
+ ReplaceModalHelper.confirmReplace();
+
+ browser.waitForAngularEnabled(false);
+ // -active file
+ EditorHelper.retrieveNavigatorActiveFile()
+ .then((list: any) => {
+ list.length.should.equal(1);
+ list.should.contain('Access Control\npermissions.acl');
browser.waitForAngularEnabled(true);
- // Check file list unchanged
- EditorHelper.retrieveNavigatorFileNames()
- .then( (list: any) => {
- // No new file (names)
- list.length.should.be.equal(startFiles.length);
- // Previous files should still exist
- startFiles.forEach((element) => {
- list.includes(element).should.be.true;
- });
+ });
+ // Check code mirror for new contents (we only have one permissions file)
+ EditorFileHelper.retrieveEditorCodeMirrorText()
+ .then((text) => {
+ text.should.contain('description: "Newly imported ACL file"');
+ });
+
+ // Check file list unchanged
+ EditorHelper.retrieveNavigatorFileNames()
+ .then( (list: any) => {
+ // Previous files should still exist
+ startFiles.forEach((element) => {
+ list.should.contain(element);
});
- // -deploy enabled
+ });
+
+ // -deploy enabled
+ EditorHelper.retrieveNavigatorFileActionButtons()
+ .then((array: any) => {
+ array[1].enabled.should.be.equal(true);
+ });
+ // deploy new item
+ EditorHelper.deployBND()
+ .then(() => {
+ // -success message
+ browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
+ // -deploy disabled
EditorHelper.retrieveNavigatorFileActionButtons()
.then((array: any) => {
- array[1].enabled.should.be.equal(true);
- });
- // deploy new item
- EditorHelper.deployBND()
- .then(() => {
- // -success message
- browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
- // -deploy disabled
- EditorHelper.retrieveNavigatorFileActionButtons()
- .then((array: any) => {
- array[1].enabled.should.be.equal(false);
- });
+ array[1].enabled.should.be.equal(false);
});
});
}));
it('should enable the addition of a Readme file via file input event', (() => {
- let startFiles;
+
+ AddFileModalHelper.selectFromFile('./e2e/data/files/importReadMe.md');
+
+ // Replace confirm modal should show
+ ReplaceModalHelper.confirmReplace();
+ // Check for new contents (we only have one readme file)
+ // -active file
+ EditorHelper.retrieveNavigatorActiveFile()
+ .then((list: any) => {
+ list.length.should.equal(1);
+ list.includes('About\nREADME.md').should.be.true;
+ });
+ EditorFileHelper.retrieveEditorText()
+ .then((text) => {
+ text.should.contain('This is the NEW readme.');
+ });
+ // Check file list unchanged
EditorHelper.retrieveNavigatorFileNames()
- .then((names) => {
- startFiles = names;
- })
- .then(() => {
- AddFileModalHelper.selectFromFile('./e2e/data/files/importReadMe.md');
- })
- .then(() => {
- // Replace confirm modal should show
- ReplaceModalHelper.confirmReplace();
- })
- .then(() => {
- // Check for new contents (we only have one readme file)
- // -active file
- EditorHelper.retrieveNavigatorActiveFile()
- .then((list: any) => {
- list.length.should.equal(1);
- list.includes('About\nREADME.md').should.be.true;
- });
- EditorFileHelper.retrieveEditorText()
- .then((text) => {
- text.should.contain('This is the NEW readme.');
- });
- // Check file list unchanged
- EditorHelper.retrieveNavigatorFileNames()
- .then( (list: any) => {
- // No new file (names)
- list.length.should.be.equal(startFiles.length);
- // Previous files should still exist
- startFiles.forEach((element) => {
- list.includes(element).should.be.true;
- });
+ .then( (list: any) => {
+ // No new file (names)
+ list.length.should.be.equal(startFiles.length);
+ // Previous files should still exist
+ startFiles.forEach((element) => {
+ list.includes(element).should.be.true;
});
- // -deploy enabled
+ });
+ // -deploy enabled
+ EditorHelper.retrieveNavigatorFileActionButtons()
+ .then((array: any) => {
+ array[1].enabled.should.be.equal(true);
+ });
+ // deploy new item
+ EditorHelper.deployBND()
+ .then(() => {
+ // -success message
+ browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
+ // -deploy disabled
EditorHelper.retrieveNavigatorFileActionButtons()
.then((array: any) => {
- array[1].enabled.should.be.equal(true);
- });
- // deploy new item
- EditorHelper.deployBND()
- .then(() => {
- // -success message
- browser.wait(ExpectedConditions.visibilityOf(element(by.id('success_notify'))), 10000);
- // -deploy disabled
- EditorHelper.retrieveNavigatorFileActionButtons()
- .then((array: any) => {
- array[1].enabled.should.be.equal(false);
- });
+ array[1].enabled.should.be.equal(false);
});
});
}));
@@ -593,7 +569,6 @@ describe('Editor Define', (() => {
});
})
.then(() => {
- let startFiles;
EditorHelper.retrieveNavigatorFileNames()
.then((names) => {
startFiles = names;
diff --git a/packages/composer-playground/e2e/utils/add-file-helper.ts b/packages/composer-playground/e2e/utils/add-file-helper.ts
index fe60b0bc4b..74784c9e9b 100644
--- a/packages/composer-playground/e2e/utils/add-file-helper.ts
+++ b/packages/composer-playground/e2e/utils/add-file-helper.ts
@@ -7,13 +7,13 @@ export class AddFileModalHelper {
// Wait for modal to appear
static waitForAddFileModalToAppear() {
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000);
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000);
}
// Cancel Add
static cancelAdd() {
// AddFile modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000)
.then(() => {
return element(by.id('add-file_cancel')).click();
});
@@ -22,7 +22,7 @@ export class AddFileModalHelper {
// Exit Add
static exitAdd() {
// AddFile modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000)
.then(() => {
return element(by.id('add-file_exit')).click();
});
@@ -31,10 +31,10 @@ export class AddFileModalHelper {
// Confirm Add
static confirmAdd() {
// AddFile modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000)
.then(() => {
let addElement = element(by.id('add-file_confirm'));
- browser.wait(ExpectedConditions.elementToBeClickable(addElement), 5000);
+ browser.wait(ExpectedConditions.elementToBeClickable(addElement), 10000);
return addElement.click();
});
}
@@ -42,10 +42,10 @@ export class AddFileModalHelper {
// Select Script file via Radio Button
static selectScriptRadioOption() {
// AddFile modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000)
.then(() => {
let selectElement = element(by.css('[for="file-type-js"]'));
- browser.wait(ExpectedConditions.elementToBeClickable(selectElement), 5000);
+ browser.wait(ExpectedConditions.elementToBeClickable(selectElement), 10000);
return selectElement.click();
});
}
@@ -53,10 +53,10 @@ export class AddFileModalHelper {
// Select Model file via Radio Button
static selectModelRadioOption() {
// AddFile modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000)
.then(() => {
let selectElement = element(by.css('[for="file-type-cto"]'));
- browser.wait(ExpectedConditions.elementToBeClickable(selectElement), 5000);
+ browser.wait(ExpectedConditions.elementToBeClickable(selectElement), 10000);
return selectElement.click();
});
}
@@ -64,13 +64,12 @@ export class AddFileModalHelper {
// Select BND from BNA file drop
static selectFromFile(filePath: string) {
// Import modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000)
.then(() => {
- // Import empty-network
let inputFileElement = element(by.id('file-importer_input'));
let importElement = element(by.id('add-file_confirm'));
dragDropFile(inputFileElement, filePath);
- browser.wait(ExpectedConditions.elementToBeClickable(importElement), 5000);
+ browser.wait(ExpectedConditions.elementToBeClickable(importElement), 10000);
return importElement.click();
});
}
diff --git a/packages/composer-playground/e2e/utils/editor-helper.ts b/packages/composer-playground/e2e/utils/editor-helper.ts
index a874b051f0..45e66f84f6 100644
--- a/packages/composer-playground/e2e/utils/editor-helper.ts
+++ b/packages/composer-playground/e2e/utils/editor-helper.ts
@@ -1,7 +1,6 @@
import { browser, element, by } from 'protractor';
import { ExpectedConditions } from 'protractor';
-
let scrollMe = (target) => {
target.scrollIntoView(true);
};
@@ -34,7 +33,7 @@ export class EditorHelper {
static retrieveNavigatorFileNames() {
// Due to scroll bar, need to scroll element into view in order to inspect text
return element(by.css('.side-bar-nav')).all(by.css('.flex-container')).map((elm) => { browser.executeScript(scrollMe, elm);
- browser.wait(ExpectedConditions.visibilityOf(elm), 5000);
+ browser.wait(ExpectedConditions.visibilityOf(elm), 10000);
return elm.getText(); });
}
@@ -61,8 +60,12 @@ export class EditorHelper {
// Retrieve current 'active' file from navigator
static retrieveNavigatorActiveFile() {
return element(by.css('.files')).all(by.css('.active')).map((elm) => { browser.executeScript(scrollMe, elm);
- browser.wait(ExpectedConditions.visibilityOf(elm), 5000);
+ browser.wait(ExpectedConditions.visibilityOf(elm), 10000);
return elm.getText(); });
}
+ // Retrieve Editor Side Navigation File Elements
+ static clickNavigatorFileItem(name: string) {
+ return element(by.css('.side-bar-nav')).all(by.css('.flex-container')).filter((element) => { return element.getText().then((text) => { return text.indexOf(name) !== -1; }); }).click();
+ }
}
diff --git a/packages/composer-playground/e2e/utils/import-helper.ts b/packages/composer-playground/e2e/utils/import-helper.ts
index 94a76b08d8..d23e1d8a49 100644
--- a/packages/composer-playground/e2e/utils/import-helper.ts
+++ b/packages/composer-playground/e2e/utils/import-helper.ts
@@ -7,14 +7,14 @@ export class ImportModalHelper {
// Select BND from BNA file drop
static selectBusinessNetworkDefinitionFromFile(filePath: string) {
// Import modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000)
.then(() => {
let inputFileElement = element(by.id('file-importer_input'));
return dragDropFile(inputFileElement, filePath);
})
.then(() => {
let importElement = element(by.id('import_confirm'));
- return browser.wait(ExpectedConditions.elementToBeClickable(importElement), 5000)
+ return browser.wait(ExpectedConditions.elementToBeClickable(importElement), 10000)
.then(() => {
return importElement.click();
});
@@ -24,7 +24,7 @@ export class ImportModalHelper {
// Confirm import
static confirmImport() {
// Import modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000)
.then(() => {
return element(by.id('import_confirm')).click();
});
@@ -33,13 +33,13 @@ export class ImportModalHelper {
// Cancel import
static cancelImport() {
// Import modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000)
.then(() => {
return element(by.id('import_cancel')).click();
});
}
static waitForImportModalToAppear() {
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 5000);
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.import'))), 10000);
}
}
diff --git a/packages/composer-playground/e2e/utils/operations-helper.ts b/packages/composer-playground/e2e/utils/operations-helper.ts
index 96039d45aa..39a7c5340a 100644
--- a/packages/composer-playground/e2e/utils/operations-helper.ts
+++ b/packages/composer-playground/e2e/utils/operations-helper.ts
@@ -9,7 +9,7 @@ export class OperationsHelper {
.then(() => {
return element(by.id('welcome_start')).click()
.then(() => {
- return browser.wait(ExpectedConditions.invisibilityOf(element(by.css('.welcome'))), 5000);
+ return browser.wait(ExpectedConditions.invisibilityOf(element(by.css('.welcome'))), 10000);
});
});
};
diff --git a/packages/composer-playground/e2e/utils/replace-helper.ts b/packages/composer-playground/e2e/utils/replace-helper.ts
index 889de912f2..b676da896a 100644
--- a/packages/composer-playground/e2e/utils/replace-helper.ts
+++ b/packages/composer-playground/e2e/utils/replace-helper.ts
@@ -6,14 +6,14 @@ export class ReplaceModalHelper {
// Cancel replace
static cancelReplace() {
// Replace modal should be present
- browser.wait(ExpectedConditions.visibilityOf(element(by.css('.replace'))), 5000);
+ browser.wait(ExpectedConditions.visibilityOf(element(by.css('.replace'))), 10000);
return element(by.id('replace_cancel')).click();
}
// Confirm Replace
static confirmReplace() {
// Replace modal should be present
- return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.replace'))), 5000)
+ return browser.wait(ExpectedConditions.visibilityOf(element(by.css('.replace'))), 10000)
.then(() => {
return element(by.id('replace_confirm')).click();
});
diff --git a/packages/composer-playground/package.json b/packages/composer-playground/package.json
index aaf84bb42f..608f41a7e6 100644
--- a/packages/composer-playground/package.json
+++ b/packages/composer-playground/package.json
@@ -59,8 +59,7 @@
"server": "npm run server:dev",
"start:hmr": "npm run server:dev:hmr",
"start": "npm run server:dev",
- "test:unit": "karma start ./config/karma.conf.js",
- "test": "npm run test:unit && npm run e2e:main",
+ "test": "karma start ./config/karma.conf.js",
"tslint": "tslint",
"typedoc": "typedoc",
"version": "npm run build",
diff --git a/packages/composer-playground/src/app/app.component.html b/packages/composer-playground/src/app/app.component.html
index 7e780bb912..b31b2e0c63 100644
--- a/packages/composer-playground/src/app/app.component.html
+++ b/packages/composer-playground/src/app/app.component.html
@@ -6,10 +6,10 @@
-
+
-
+
diff --git a/packages/composer-playground/src/app/footer/footer.component.scss b/packages/composer-playground/src/app/footer/footer.component.scss
index 00140b9c70..117519e48a 100644
--- a/packages/composer-playground/src/app/footer/footer.component.scss
+++ b/packages/composer-playground/src/app/footer/footer.component.scss
@@ -5,18 +5,17 @@ app-footer {
position: relative;
display: flex;
width: 100%;
- bottom: -($space-large);
.footer-main{
position: relative;
bottom: 0;
flex: 1;
- padding: 22px 10px 22px 10px;
+ padding: 20px 10px 20px 10px;
border-top: solid 1px #D0DADA;
color: $secondary-text ;
font-size: 12px;
display: flex;
- height: 62px;
+ height: 55px;
.footer-left, .footer-right{
flex:1;
diff --git a/packages/composer-playground/src/assets/styles/components/_side-bar-nav.scss b/packages/composer-playground/src/assets/styles/components/_side-bar-nav.scss
index 016f28e649..f1f0dd8473 100644
--- a/packages/composer-playground/src/assets/styles/components/_side-bar-nav.scss
+++ b/packages/composer-playground/src/assets/styles/components/_side-bar-nav.scss
@@ -38,6 +38,6 @@
.main-view {
background-color: $fourth-highlight;
width: 100%;
- padding: $space-large;
+ padding: $space-large $space-large 0 $space-large;
}
}
diff --git a/packages/composer-runtime-embedded/lib/embeddeddataservice.js b/packages/composer-runtime-embedded/lib/embeddeddataservice.js
index 77c1a303dc..1e6bc91766 100644
--- a/packages/composer-runtime-embedded/lib/embeddeddataservice.js
+++ b/packages/composer-runtime-embedded/lib/embeddeddataservice.js
@@ -19,6 +19,9 @@ const Logger = require('composer-common').Logger;
const LOG = Logger.getLog('EmbeddedDataService');
+// Install the PouchDB plugins. The order of the adapters is important!
+PouchDBDataService.registerPouchDBPlugin(require('pouchdb-adapter-memory'));
+
/**
* Base class representing the data service provided by a {@link Container}.
* @protected
diff --git a/packages/composer-runtime-embedded/package.json b/packages/composer-runtime-embedded/package.json
index 62b316a4ae..2ee4eba539 100644
--- a/packages/composer-runtime-embedded/package.json
+++ b/packages/composer-runtime-embedded/package.json
@@ -57,6 +57,7 @@
"composer-runtime-pouchdb": "^0.9.2",
"debug": "^2.6.2",
"istanbul-lib-instrument": "^1.7.2",
+ "pouchdb-adapter-memory": "^6.2.0",
"request": "^2.81.0",
"uuid": "^3.0.1"
},
diff --git a/packages/composer-runtime-pouchdb/lib/pouchdbdataservice.js b/packages/composer-runtime-pouchdb/lib/pouchdbdataservice.js
index ee1dfac452..f7b55858a3 100644
--- a/packages/composer-runtime-pouchdb/lib/pouchdbdataservice.js
+++ b/packages/composer-runtime-pouchdb/lib/pouchdbdataservice.js
@@ -24,9 +24,6 @@ const PouchDBUtils = require('./pouchdbutils');
const LOG = Logger.getLog('PouchDBDataService');
// Install the PouchDB plugins. The order of the adapters is important!
-PouchDB.plugin(require('pouchdb-adapter-idb'));
-PouchDB.plugin(require('pouchdb-adapter-websql'));
-PouchDB.plugin(require('pouchdb-adapter-memory'));
PouchDB.plugin(require('pouchdb-find'));
// This is the object type used to form composite keys for the collection of collections.
@@ -38,6 +35,16 @@ const collectionObjectType = '$syscollections';
*/
class PouchDBDataService extends DataService {
+ /**
+ * Register the specified PouchDB plugin with PouchDB.
+ * @param {*} plugin The PouchDB plugin to register.
+ */
+ static registerPouchDBPlugin(plugin) {
+ // No logging here as this is called during static initialization
+ // at startup, and we don't want to try and load the logger yet.
+ PouchDB.plugin(plugin);
+ }
+
/**
* Create a new instance of PouchDB.
* @param {string} name The name of the PouchDB database.
diff --git a/packages/composer-runtime-pouchdb/package.json b/packages/composer-runtime-pouchdb/package.json
index 76e96e0587..d055718d82 100644
--- a/packages/composer-runtime-pouchdb/package.json
+++ b/packages/composer-runtime-pouchdb/package.json
@@ -56,9 +56,6 @@
"composer-runtime": "^0.9.2",
"debug": "^2.6.2",
"istanbul-lib-instrument": "^1.7.2",
- "pouchdb-adapter-idb": "^6.2.0",
- "pouchdb-adapter-memory": "^6.2.0",
- "pouchdb-adapter-websql": "^6.2.0",
"pouchdb-collate": "^6.2.0",
"pouchdb-core": "^6.2.0",
"pouchdb-find": "^6.2.0",
diff --git a/packages/composer-runtime-pouchdb/test/pouchdbdataservice.js b/packages/composer-runtime-pouchdb/test/pouchdbdataservice.js
index a45a62c0f5..bdf117af1a 100644
--- a/packages/composer-runtime-pouchdb/test/pouchdbdataservice.js
+++ b/packages/composer-runtime-pouchdb/test/pouchdbdataservice.js
@@ -59,6 +59,20 @@ describe('PouchDBDataService', () => {
return dataService.destroy();
});
+ describe('#registerPouchDBPlugin', () => {
+
+ beforeEach(() => {
+ sandbox.stub(PouchDB, 'plugin');
+ });
+
+ it('should register a PouchDB plugin', () => {
+ PouchDBDataService.registerPouchDBPlugin({ foo: 'bar' });
+ sinon.assert.calledOnce(PouchDB.plugin);
+ sinon.assert.calledWith(PouchDB.plugin, { foo: 'bar' });
+ });
+
+ });
+
describe('#createPouchDB', () => {
beforeEach(() => {
diff --git a/packages/composer-runtime-web/lib/webdataservice.js b/packages/composer-runtime-web/lib/webdataservice.js
index 158e17d100..570444350c 100644
--- a/packages/composer-runtime-web/lib/webdataservice.js
+++ b/packages/composer-runtime-web/lib/webdataservice.js
@@ -19,6 +19,10 @@ const Logger = require('composer-common').Logger;
const LOG = Logger.getLog('WebDataService');
+// Install the PouchDB plugins. The order of the adapters is important!
+PouchDBDataService.registerPouchDBPlugin(require('pouchdb-adapter-idb'));
+PouchDBDataService.registerPouchDBPlugin(require('pouchdb-adapter-websql'));
+
/**
* Base class representing the data service provided by a {@link Container}.
* @protected
diff --git a/packages/composer-runtime-web/package.json b/packages/composer-runtime-web/package.json
index c5c73bfdbf..3865735506 100644
--- a/packages/composer-runtime-web/package.json
+++ b/packages/composer-runtime-web/package.json
@@ -65,6 +65,8 @@
"composer-common": "^0.9.2",
"composer-runtime": "^0.9.2",
"composer-runtime-pouchdb": "^0.9.2",
+ "pouchdb-adapter-idb": "^6.2.0",
+ "pouchdb-adapter-websql": "^6.2.0",
"uuid": "^3.0.1",
"xhr": "^2.4.0"
}
diff --git a/packages/composer-runtime/test/data/mozart.cto b/packages/composer-runtime/test/data/mozart.cto
index 4465830766..07cad6ec78 100644
--- a/packages/composer-runtime/test/data/mozart.cto
+++ b/packages/composer-runtime/test/data/mozart.cto
@@ -103,7 +103,7 @@ asset Business identified by sbi {
/**
* An abstract transaction type for animal movements
*/
-abstract transaction AnimalMovement identified by transactionId {
+abstract transaction AnimalMovement {
o String transactionId
o String[] logs optional
--> Animal animal
diff --git a/packages/composer-systests/systest/identities.js b/packages/composer-systests/systest/identities.js
index ba97e4f077..fca10407cb 100644
--- a/packages/composer-systests/systest/identities.js
+++ b/packages/composer-systests/systest/identities.js
@@ -58,6 +58,13 @@ describe('Identity system tests', () => {
return TestUtil.getClient('systest-identities')
.then((result) => {
client = result;
+ return client.getIdentityRegistry()
+ .then((ir) => {
+ return ir.getAll();
+ })
+ .then((all) => {
+ console.log(all);
+ });
});
});
});
diff --git a/packages/composer-website/jekylldocs/tutorials/developer-guide.md b/packages/composer-website/jekylldocs/tutorials/developer-guide.md
index 13f99c301d..14f61bc913 100644
--- a/packages/composer-website/jekylldocs/tutorials/developer-guide.md
+++ b/packages/composer-website/jekylldocs/tutorials/developer-guide.md
@@ -10,13 +10,13 @@ excerpt: "The developer guide will walk you through the steps required to build
# Developer Tutorial for creating a {{site.data.conrefs.composer_full}} solution
-*Note:* this tutorial was written against {{site.data.conrefs.composer_full}} v0.8 on Ubuntu Linux running with {{site.data.conrefs.hlf_full}} v1.0 where referenced below and also tested for a Mac environment. (The relevant steps for a {{site.data.conrefs.hlf_full}} v0.6 setup are shown in *italics*).
+*Note:* this tutorial was written against the current release of {{site.data.conrefs.composer_full}} on Ubuntu Linux running with {{site.data.conrefs.hlf_full}} v1.0 where referenced below and also tested on a Mac environment. (Note: the relevant steps for a {{site.data.conrefs.hlf_full}} v0.6 setup are shown in *italics*).
This tutorial will walk you through the steps required to build a {{site.data.conrefs.composer_full}} blockchain solution from scratch. In the space of a day or probably less, you will be able to go from an idea for a disruptive blockchain innovation, to executing transactions against a real {{site.data.conrefs.hlf_full}} blockchain network, and generating/running a sample Angular 2 based application for Commodity Trading that interacts with a blockchain network.
## Install {{site.data.conrefs.composer_full}}
-First, make sure you have installed {{site.data.conrefs.composer_full}}. Follow this [Development Environment Install guide](../installing/development-tools.html) - As well as installing Composer, it has instructions to quickly build your {{site.data.conrefs.hlf_full}} blockchain environment (using Docker containers) which we will use later on in this guide. It includes the installation of the Yeoman app generator and some pre-requisite Angular 2 packages.
+First, make sure you have installed {{site.data.conrefs.composer_full}}. Follow this [Development Environment Install guide](../installing/development-tools.html) - As well as installing Composer, it has instructions to quickly build your {{site.data.conrefs.hlf_full}} blockchain environment (using Docker containers) which we will use later on in this guide. It includes the installation of the Yeoman application generator and some pre-requisite Angular 2 packages.
## Install an Editor (eg. VSCode - and its {{site.data.conrefs.composer_full}} Extension for Syntax Highlighting)
@@ -30,7 +30,7 @@ After installation, launch VSCode and select `View > Command Palette...` then ty
## Create a Business Network Definition
-The key concept for Composer is the **business network definition (BND)**. It defines the data model, business (and therein transaction) logic and access control rules for your blockchain solution. It executes on {{site.data.conrefs.hlf_full}}. To create a BND, we need to create a suitable project structure on disk.
+The key concept for Composer is the **business network definition (BND)**. It defines the data model, business (and therein transaction) logic and access control rules for your blockchain solution. It executes on {{site.data.conrefs.hlf_full}}. To create a BND, you need to create a suitable project structure on disk.
The easiest way to get started is to clone an **existing sample business network**. Open up a command prompt and clone the Composer sample networks GitHub repository. For Linux, perform this as a non-root user.
@@ -44,7 +44,7 @@ Then, make a copy of this directory in your project, called 'my-network'.
cp -r ./composer-sample-networks/packages/basic-sample-network/ ./my-network
```
-You should now have a folder called `my-network` (as the basis for our project) that we can start to modify. Using VSCode, open the `my-network` folder using Explorer in VSCode, click OK to open the folder. You should see the file layout in the explorer pane.
+You should now have a folder called `my-network` (as the basis for our project) that we can start to modify. Using VSCode, open the `my-network` folder click on Explorer in VSCode, click OK to open the folder. You should see the file layout in the explorer pane.