Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve validation for new connectors #1095

Merged
merged 1 commit into from
Nov 25, 2020
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
4 changes: 4 additions & 0 deletions packages/caliper-fabric/lib/FabricConnectorFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ const connectorFactory = async (workerIndex) => {
const loadedConnectorConfiguration = CaliperUtils.parseYaml(connectorConfigurationFile);
const legacyVersion = loadedConnectorConfiguration.version === '1.0';

if (!legacyVersion && !semver.satisfies(loadedConnectorConfiguration.version, '=2.0')) {
throw new Error(`Unknown network configuration version ${loadedConnectorConfiguration.version} specified`);
}

const installedNodeSDKVersion = _determineInstalledNodeSDKVersion();
const useGateway = ConfigUtil.get(ConfigUtil.keys.Fabric.Gateway.Enabled, false);
const useDiscovery = ConfigUtil.get(ConfigUtil.keys.Fabric.Gateway.Discovery, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const CaliperUtils = require('@hyperledger/caliper-core').CaliperUtils;
const ConnectionProfileDefinition = require('./ConnectionProfileDefinition');
const fs = require('fs');

const Logger = CaliperUtils.getLogger('ConnectorConfiguration');

/**
* Parse and process the fabric connector configuration
*/
Expand Down Expand Up @@ -162,6 +164,10 @@ class ConnectorConfiguration {
throw new Error(`No connection profile entry for organization ${mspId} has been defined`);
}

if (!connectionProfileEntry.path) {
throw new Error(`No path for the connection profile for organization ${mspId} has been defined`);
}

if (!connectionProfileEntry.loadedConnectionProfile) {
connectionProfileEntry.loadedConnectionProfile = await this._loadConnectionProfile(connectionProfileEntry.path);
}
Expand Down Expand Up @@ -217,10 +223,14 @@ class ConnectorConfiguration {
getAliasNameForOrganizationAndIdentityName(mspId, identityName) {
if (!identityName || identityName.length === 0) {
if (!mspId || mspId.length === 0) {
Logger.debug(`Selecting invoker ${this.defaultInvokerForDefaultOrganization} for default organization`);

return this.defaultInvokerForDefaultOrganization;
}
const invokerForOrganization = this.defaultInvokerMap.get(mspId);
Logger.debug(`Selecting invoker ${invokerForOrganization} for organization ${mspId}`);

return this.defaultInvokerMap.get(mspId);
return invokerForOrganization;
}

return this.identityManager.generateAliasNameFromOrganizationAndIdentityName(mspId, identityName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class V1Fabric extends ConnectorBase {
*/
async getContext(roundIndex, args) {
if (!this.MapsFromConnectionProfileCreated) {
await this._validateConnectionProfilesAreStatic();
this.MapsFromConnectionProfileCreated = true;
this.orderersInChannelMap = await this.connectorConfiguration.getOrderersInChannelMap();
this.peersInChannelByOrganizationMap = await this.connectorConfiguration.getEndorsingPeersInChannelByOrganizationMap();
Expand All @@ -124,6 +125,7 @@ class V1Fabric extends ConnectorBase {
* @async
*/
async init() {
await this._validateConnectionProfilesAreStatic();
const defaultOrganization = this.connectorConfiguration.getOrganizations()[0];
const tlsInfo = this.connectorConfiguration.isMutualTLS() ? 'mutual'
: (this.connectorConfiguration.getConnectionProfileDefinitionForOrganization(defaultOrganization).isTLSEnabled() ? 'server' : 'none');
Expand Down Expand Up @@ -203,6 +205,25 @@ class V1Fabric extends ConnectorBase {
// INTERNAL UTILITY FUNCTIONS //
////////////////////////////////

/**
* Validate that the connection profiles are static
*/
async _validateConnectionProfilesAreStatic() {
const organizations = this.connectorConfiguration.getOrganizations();
const incorrectConnectionProfileDefinitions = [];
for (const organization of organizations) {
const connectionProfileDefinitionForOrganization = await this.connectorConfiguration.getConnectionProfileDefinitionForOrganization(organization);

if (connectionProfileDefinitionForOrganization.isDynamicConnectionProfile()) {
incorrectConnectionProfileDefinitions.push(organization);
}
}

if (incorrectConnectionProfileDefinitions.length > 0) {
throw new Error(`Connection profiles for the organization(s) '${incorrectConnectionProfileDefinitions.join(', ')}' have been specified as discover which is not allowed`);
}
}

/**
* Initialize channels for each Client instance where that channel is defined for that client
* @private
Expand Down
45 changes: 26 additions & 19 deletions packages/caliper-fabric/lib/identity-management/IdentityManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
const CaliperUtils = require('@hyperledger/caliper-core').CaliperUtils;
const fs = require('fs').promises;

const Logger = CaliperUtils.getLogger('IdentityManager');

/**
* A class to handle identities defined via the connector configuration
*/
Expand Down Expand Up @@ -136,6 +138,8 @@ class IdentityManager {
*/
async _addToWallet(mspId, identityName, isAdmin, certificate, privateKey) {
const alias = this.generateAliasNameFromOrganizationAndIdentityName(mspId, identityName);
Logger.info(`Adding ${identityName} (admin=${isAdmin}) as ${alias} for organization ${mspId}`);

try {
await this.inMemoryWalletFacade.import(mspId, alias, certificate, privateKey);
if (isAdmin) {
Expand Down Expand Up @@ -226,11 +230,11 @@ class IdentityManager {
try {
const statOfpath = await fs.stat(walletPath);
if (!statOfpath.isDirectory()) {
throw new Error(`The path property ${walletPath} does not point to a directory for ${mspId}`);
throw new Error(`The wallet path property ${walletPath} does not point to a directory for ${mspId}`);
}
} catch(err) {
if (err.errno === -2 || err.errno === -4058) {
throw new Error(`The path property ${walletPath} does not point to an existing directory for ${mspId}`);
throw new Error(`The wallet path property ${walletPath} does not point to an existing directory for ${mspId}`);
}
throw err;
}
Expand All @@ -257,18 +261,18 @@ class IdentityManager {
* @private
*/
async _extractIdentitiesFromCertificateAndPrivateKeyArray(mspId, certificates) {
if (!Array.isArray(certificates)) {
throw new Error('certificates property must be an array');
if (!Array.isArray(certificates) || certificates.length === 0) {
throw new Error(`No valid entries in certificates property for organization ${mspId}`);
}

for (const identity of certificates) {

if (!identity.name || !identity.clientSignedCert || !identity.clientPrivateKey) {
throw new Error('A valid entry in certificates must have an name, clientSignedCert and clientPrivateKey entry');
throw new Error(`A valid entry in certificates for organization ${mspId} must have a name, clientSignedCert and clientPrivateKey entry`);
}

const certificate = await this._extractPEMFromPathOrPEMDefinition(identity.clientSignedCert, 'clientSignedCert', identity.name);
const privateKey = await this._extractPEMFromPathOrPEMDefinition(identity.clientPrivateKey, 'clientPrivateKey', identity.name);
const certificate = await this._extractPEMFromPathOrPEMDefinition(identity.clientSignedCert, 'clientSignedCert', identity.name, mspId);
const privateKey = await this._extractPEMFromPathOrPEMDefinition(identity.clientPrivateKey, 'clientPrivateKey', identity.name, mspId);
const isAdmin = (identity.admin !== undefined && (identity.admin === true || identity.admin === 'true'));
await this._addToWallet(mspId, identity.name, isAdmin, certificate, privateKey);
}
Expand All @@ -279,20 +283,21 @@ class IdentityManager {
*
* @param {*} CertificateOrPrivateKeyDefinition The clientSignedCert or clientPrivateKey property in the configuration
* @param {string} propertyNameBeingProcessed A string of the provided property
* @param {string} name The name associated with this identity
* @param {string} identityName The name associated with this identity
* @param {string} mspId mspId of the organisation
* @returns {Promise<string>} the PEM
* @async
* @private
*/
async _extractPEMFromPathOrPEMDefinition(CertificateOrPrivateKeyDefinition, propertyNameBeingProcessed, name) {
async _extractPEMFromPathOrPEMDefinition(CertificateOrPrivateKeyDefinition, propertyNameBeingProcessed, identityName, mspId) {
let pem;

if (CertificateOrPrivateKeyDefinition.path) {
pem = await this._extractPEMFromPath(CertificateOrPrivateKeyDefinition.path, propertyNameBeingProcessed, name);
pem = await this._extractPEMFromPath(CertificateOrPrivateKeyDefinition.path, propertyNameBeingProcessed, identityName, mspId);
} else if (CertificateOrPrivateKeyDefinition.pem) {
pem = this._extractPEMFromPEM(CertificateOrPrivateKeyDefinition.pem, propertyNameBeingProcessed, name);
pem = this._extractPEMFromPEM(CertificateOrPrivateKeyDefinition.pem, propertyNameBeingProcessed, identityName, mspId);
} else {
throw new Error(`No path or pem property specified for ${propertyNameBeingProcessed} for name ${name}`);
throw new Error(`No path or pem property specified for ${propertyNameBeingProcessed} for name ${identityName} in organization ${mspId}`);
}

return pem;
Expand All @@ -303,27 +308,28 @@ class IdentityManager {
*
* @param {string} pathToPEMFile The path to the file containing the PEM information
* @param {string} propertyNameBeingProcessed A string of the provided property
* @param {string} name The name associated with this identity
* @param {string} identityName The name associated with this identity
* @param {string} mspId mspId of the organisation
* @returns {Promise<string>} the PEM
* @async
* @private
*/
async _extractPEMFromPath(pathToPEMFile, propertyNameBeingProcessed, name) {
async _extractPEMFromPath(pathToPEMFile, propertyNameBeingProcessed, identityName, mspId) {
const configPath = CaliperUtils.resolvePath(pathToPEMFile);

try {
await fs.stat(configPath);
} catch(err) {
if (err.errno === -2 || err.errno === -4058) {
throw new Error(`path property does not point to a file that exists for ${propertyNameBeingProcessed} for name ${name}`);
throw new Error(`path property does not point to a file that exists for ${propertyNameBeingProcessed} for name ${identityName} in organization ${mspId}`);
}
throw err;
}

const pem = (await fs.readFile(configPath)).toString();

if (!pem.startsWith('-----BEGIN ')) {
throw new Error(`path property does not point to a valid pem file for ${propertyNameBeingProcessed} for name ${name}`);
throw new Error(`path property does not point to a valid pem file for ${propertyNameBeingProcessed} for name ${identityName} in organization ${mspId}`);
}

return pem;
Expand All @@ -334,20 +340,21 @@ class IdentityManager {
*
* @param {string} pem the embedded pem property value
* @param {string} propertyNameBeingProcessed A string of the provided property
* @param {string} name The name associated with this identity
* @param {string} identityName The name associated with this identity
* @param {string} mspId mspId of the organisation
* @returns {Promise<string>} the PEM
* @async
* @private
*/
_extractPEMFromPEM(pem, propertyNameBeingProcessed, name) {
_extractPEMFromPEM(pem, propertyNameBeingProcessed, identityName, mspId) {
if (pem.startsWith('-----BEGIN ')) {
return pem;
}

const decodedPEM = Buffer.from(pem, 'base64').toString();

if (!decodedPEM.startsWith('-----BEGIN ')) {
throw new Error(`pem property not valid for ${propertyNameBeingProcessed} for name ${name}`);
throw new Error(`pem property not valid for ${propertyNameBeingProcessed} for name ${identityName} in organization ${mspId}`);
}

return decodedPEM;
Expand Down
1 change: 1 addition & 0 deletions packages/caliper-fabric/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"nyc": {
"include": [
"lib/configValidator.js",
"lib/FabricConnectorFactory.js",
"lib/connector-configuration",
"lib/identity-management",
"lib/connector-versions/v2",
Expand Down
13 changes: 13 additions & 0 deletions packages/caliper-fabric/test/FabricConnectorFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const { ConfigUtil } = require('@hyperledger/caliper-core');

const legacyConfig = './sample-configs/LegacyNetworkConfig.yaml';
const v2Config = './sample-configs/NoIdentitiesNetworkConfig.yaml';
const unknownVersionConfig = './sample-configs/UnknownVersionConfig.yaml';

const DefaultEventHandlerStrategies = {};
const DefaultQueryHandlerStrategies = {};
Expand Down Expand Up @@ -213,4 +214,16 @@ describe('A Fabric Connector Factory', () => {
await ConnectorFactory(1).should.be.rejectedWith(/Installed SDK version 3.0.0 did not match any compatible Fabric connectors/);
mockery.deregisterAll();
});

it('should throw an error version if not for legacy or the new connector', async () => {
mockery.registerMock('fabric-network', {});
mockery.registerMock('fabric-network/package', {version: '1.4.11'});
mockery.registerMock('fabric-client', {});
mockery.registerMock('fabric-client/package', {version: '1.4.11'});
ConfigUtil.set(ConfigUtil.keys.NetworkConfig, path.resolve(__dirname, unknownVersionConfig));
ConfigUtil.set(ConfigUtil.keys.Fabric.Gateway.Enabled, true);
await ConnectorFactory(1).should.be.rejectedWith(/Unknown network configuration version 3.0.0 specified/);
mockery.deregisterAll();
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,38 @@ describe('A valid Connector Configuration', () => {
.should.be.rejectedWith(/No connection profile file found/);
});

it('should throw an error if the connection profile path property doesn\'t exist', async () => {
const configFile = new GenerateConfiguration(configWith2Orgs1AdminInWallet).generateConfigurationFileWithSpecifics(
{
organizations: [
{
mspid: 'Org1MSP',
identities: {
certificates: [
{
name: 'User1',
clientPrivateKey: {
pem: '-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----'
},
clientSignedCert: {
pem: '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----'
}
}
]
},
connectionProfile: {
discover: true
}
}
]
}
);
const connectorConfiguration = await new ConnectorConfigurationFactory().create(configFile, walletFacadeFactory);
await connectorConfiguration.getConnectionProfileDefinitionForOrganization('Org1MSP')
.should.be.rejectedWith(/No path for the connection profile for organization Org1MSP has been defined/);
});


it('should throw an error if the requested organization doesn\'t exist', async () => {
const configFile = new GenerateConfiguration().generateConfigurationFileWithSpecifics(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const mockery = require('mockery');
const path = require('path');

const configWith2Orgs1AdminInWallet = '../../sample-configs/BasicConfigWithStaticCCP.yaml';
const configWith2Orgs1AdminInWalletWithDiscover = '../../sample-configs/BasicConfig.yaml';

const { Client, Channel, ChannelEventHub, Constants } = require('./ClientStubs');
const GenerateConfiguration = require('../../utils/GenerateConfiguration');
Expand Down Expand Up @@ -77,7 +78,13 @@ describe('A Node-SDK V1 Fabric Non Gateway', () => {
await fabricNonGateway.init().should.not.be.rejected;
});

it('should do nothing when attempting to install a smart contract', async () => {
it('should throw an error if the connection profile is defined with discover when initalizing for use by caliper master', async () => {
const connectorConfiguration = await new ConnectorConfigurationFactory().create(path.resolve(__dirname, configWith2Orgs1AdminInWalletWithDiscover), stubWalletFacadeFactory);
const fabricNonGateway = new FabricNonGateway(connectorConfiguration, 1, 'fabric');
await fabricNonGateway.init().should.be.rejectedWith(/Connection profiles for the organization\(s\).*Org1MSP.*have been specified as discover which is not allowed/);
});

it('should do nothing when requested install a smart contract when the configuration doesn\'t require it', async () => {
const connectorConfiguration = await new ConnectorConfigurationFactory().create(path.resolve(__dirname, configWith2Orgs1AdminInWallet), stubWalletFacadeFactory);
const fabricNonGateway = new FabricNonGateway(connectorConfiguration, 1, 'fabric');
await fabricNonGateway.installSmartContract().should.not.be.rejected;
Expand All @@ -98,6 +105,13 @@ describe('A Node-SDK V1 Fabric Non Gateway', () => {
context2.should.equal(context);
});

it('should throw an error if the connection profile is defined with discover when getting a context', async () => {
const connectorConfiguration = await new ConnectorConfigurationFactory().create(path.resolve(__dirname, configWith2Orgs1AdminInWalletWithDiscover), stubWalletFacadeFactory);
const fabricNonGateway = new FabricNonGateway(connectorConfiguration, 1, 'fabric');
await fabricNonGateway.getContext().should.be.rejectedWith(/Connection profiles for the organization\(s\).*Org1MSP.*have been specified as discover which is not allowed/);
});


it('should create Clients and set tls identity when a context is first requested', async () => {
const connectorConfiguration = await new ConnectorConfigurationFactory().create(path.resolve(__dirname, configWith2Orgs1AdminInWallet), stubWalletFacadeFactory);
const fabricNonGateway = new FabricNonGateway(connectorConfiguration, 1, 'fabric');
Expand Down
Loading