Skip to content
This repository was archived by the owner on Mar 8, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/composer-cli/lib/cmds/identity/bindCommand.js
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 1 addition & 1 deletion packages/composer-cli/lib/cmds/identity/issueCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
93 changes: 93 additions & 0 deletions packages/composer-cli/lib/cmds/identity/lib/bind.js
Original file line number Diff line number Diff line change
@@ -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');

/**
* <p>
* Composer "identity bind" command
* </p>
* <p><a href="diagrams/Deploy.svg"><img src="diagrams/deploy.svg" style="width:100%;"/></a></p>
* @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;
2 changes: 1 addition & 1 deletion packages/composer-cli/lib/cmds/identity/revokeCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
8 changes: 4 additions & 4 deletions packages/composer-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
138 changes: 138 additions & 0 deletions packages/composer-cli/test/identity/bind.js
Original file line number Diff line number Diff line change
@@ -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/);
});

});
10 changes: 6 additions & 4 deletions packages/composer-runtime/lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions packages/composer-runtime/test/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down