diff --git a/.travis/before-install.sh b/.travis/before-install.sh index 1bab9dd1ee..587405c582 100755 --- a/.travis/before-install.sh +++ b/.travis/before-install.sh @@ -10,7 +10,7 @@ sudo rm /usr/local/bin/docker-compose curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose chmod +x docker-compose sudo mv docker-compose /usr/local/bin -echo "Docker-compose version: " +echo "Docker-compose version: " docker-compose --version # Update docker @@ -22,7 +22,7 @@ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" sudo apt-get update sudo apt-get install docker-ce -echo "Docker version: " +echo "Docker version: " docker --version # Grab the parent (root) directory. diff --git a/lerna.json b/lerna.json index 6ae43f72ea..5823eae946 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "packages/*" ], - "version": "0.8.2", + "version": "0.9.0", "hoist": true } diff --git a/package.json b/package.json index 2e85b9af18..f7b53fd322 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "name": "composer", "description": "You must install [Lerna](https://lernajs.io) to build this multi-package repository.", - "version": "0.8.2", + "version": "0.9.0", "main": "index.js", "private": true, "scripts": { diff --git a/packages/composer-admin/package.json b/packages/composer-admin/package.json index eda228a6b7..80feb08824 100644 --- a/packages/composer-admin/package.json +++ b/packages/composer-admin/package.json @@ -1,6 +1,6 @@ { "name": "composer-admin", - "version": "0.8.2", + "version": "0.9.0", "description": "Hyperledger Composer Admin, code that manages business networks deployed to Hyperledger Fabric", "engines": { "node": ">=6", @@ -42,9 +42,9 @@ "sinon-as-promised": "^4.0.2" }, "dependencies": { - "composer-common": "^0.8.2", - "composer-connector-hlf": "^0.8.2", - "composer-connector-hlfv1": "^0.8.2" + "composer-common": "^0.9.0", + "composer-connector-hlf": "^0.9.0", + "composer-connector-hlfv1": "^0.9.0" }, "license-check-config": { "src": [ diff --git a/packages/composer-cli/package.json b/packages/composer-cli/package.json index 6b431aea65..e68304f948 100644 --- a/packages/composer-cli/package.json +++ b/packages/composer-cli/package.json @@ -1,6 +1,6 @@ { "name": "composer-cli", - "version": "0.8.2", + "version": "0.9.0", "description": "Hyperledger Composer command line interfaces (CLIs)", "engines": { "node": ">=6", @@ -42,10 +42,10 @@ "dependencies": { "chalk": "^1.1.3", "cli-table": "^0.3.1", - "composer-admin": "^0.8.2", - "composer-client": "^0.8.2", - "composer-common": "^0.8.2", - "composer-rest-server": "^0.8.2", + "composer-admin": "^0.9.0", + "composer-client": "^0.9.0", + "composer-common": "^0.9.0", + "composer-rest-server": "^0.9.0", "homedir": "^0.6.0", "npm-paths": "^0.1.3", "nunjucks": "^3.0.0", diff --git a/packages/composer-client/package.json b/packages/composer-client/package.json index c23d29af18..c867c57835 100644 --- a/packages/composer-client/package.json +++ b/packages/composer-client/package.json @@ -1,6 +1,6 @@ { "name": "composer-client", - "version": "0.8.2", + "version": "0.9.0", "description": "The node.js client library for Hyperledger Composer, a development framework for Hyperledger Fabric", "engines": { "node": ">=6", @@ -42,9 +42,9 @@ "logError": true }, "dependencies": { - "composer-common": "^0.8.2", - "composer-connector-hlf": "^0.8.2", - "composer-connector-hlfv1": "^0.8.2", + "composer-common": "^0.9.0", + "composer-connector-hlf": "^0.9.0", + "composer-connector-hlfv1": "^0.9.0", "uuid": "^3.0.1" }, "devDependencies": { diff --git a/packages/composer-common/index.js b/packages/composer-common/index.js index 6e1139c65c..ecaff0e7a4 100644 --- a/packages/composer-common/index.js +++ b/packages/composer-common/index.js @@ -96,6 +96,7 @@ module.exports.Sort = require('./lib/query/sort'); module.exports.TransactionDeclaration = require('./lib/introspect/transactiondeclaration'); module.exports.TypescriptVisitor = require('./lib/codegen/fromcto/typescript/typescriptvisitor'); module.exports.Util = require('./lib/util'); +module.exports.ModelUtil = require('./lib/modelutil'); module.exports.Wallet = require('./lib/wallet'); module.exports.Where = require('./lib/query/where'); module.exports.Writer = require('./lib/codegen/writer.js'); diff --git a/packages/composer-common/lib/acl/aclrule.js b/packages/composer-common/lib/acl/aclrule.js index 4eacd6fc82..45b5d4cffe 100644 --- a/packages/composer-common/lib/acl/aclrule.js +++ b/packages/composer-common/lib/acl/aclrule.js @@ -112,17 +112,27 @@ class AclRule { const foundVerbs = {}; this.verbs.forEach((verb) => { if (foundVerbs[verb]) { - throw new Error(`The verb '${verb}' has been specified more than once in the ACL rule '${this.name}'`); + throw new IllegalModelException(`The verb '${verb}' has been specified more than once in the ACL rule '${this.name}'`); } foundVerbs[verb] = true; }); if(this.participant) { this.participant.validate(); + + let participantClassDeclaration = this.participant.getClassDeclaration(); + if (participantClassDeclaration && participantClassDeclaration.constructor.name !== 'ParticipantDeclaration') { + throw new IllegalModelException(`The participant '${participantClassDeclaration.getName()}' must be a participant`); + } } if(this.transaction) { this.transaction.validate(); + + let transactionClassDeclaration = this.transaction.getClassDeclaration(); + if (transactionClassDeclaration && transactionClassDeclaration.constructor.name !== 'TransactionDeclaration') { + throw new IllegalModelException(`The transaction '${transactionClassDeclaration.getName()}' must be a transaction`); + } } if(this.predicate) { diff --git a/packages/composer-common/lib/acl/modelbinding.js b/packages/composer-common/lib/acl/modelbinding.js index ccc070c2d4..13b68a80d0 100644 --- a/packages/composer-common/lib/acl/modelbinding.js +++ b/packages/composer-common/lib/acl/modelbinding.js @@ -142,70 +142,43 @@ class ModelBinding { /** * Semantic validation of the structure of this ModelBinding. - *

- * Algorithm: - *

- * - *
-     * We assume we have ns.class.property and try to resolve the class in ns
-     * - On success
-     * -- Check that the property exists
-     * - On failure
-     * -- Try to resolve ns.class
-     * -- On failure
-     * --- If instanceId and variableName are null
-     * --- Try to resolve ns
-     * 
+ * * @throws {IllegalModelException} * @private */ validate() { const mm = this.getAclRule().getAclFile().getModelManager(); - // assume qualifiedName is ns.class.property - const nsDotClass = ModelUtil.getNamespace(this.qualifiedName); - const ns = ModelUtil.getNamespace(nsDotClass); - const className = ModelUtil.getShortName(nsDotClass); - const propertyName = ModelUtil.getShortName(this.qualifiedName); - const modelFile = mm.getModelFile(ns); + const ns = ModelUtil.getNamespace(this.qualifiedName); + if (ModelUtil.isRecursiveWildcardName(this.qualifiedName)) { + const namespaces = mm.getNamespaces(); - if(modelFile) { - const classDeclaration = modelFile.getLocalType(className); - if(!classDeclaration) { - throw new Error('Failed to find class ' + nsDotClass); - } - this.classDeclaration = classDeclaration; - const property = classDeclaration.getProperty(propertyName); - if(!property) { - throw new Error('Failed to find property ' + this.qualifiedName); + if (namespaces.findIndex(function (element, index, array) { + return (ns === element || element.startsWith(ns + '.')); + })=== -1) { + throw new IllegalModelException('Failed to find namespace ' + this.qualifiedName); } - } - else { - // assume qualifiedName is ns.class - const ns = ModelUtil.getNamespace(this.qualifiedName); - const className = ModelUtil.getShortName(this.qualifiedName); + } else if (ModelUtil.isWildcardName(this.qualifiedName)) { const modelFile = mm.getModelFile(ns); - if(modelFile) { - const classDeclaration = modelFile.getLocalType(className); - if(!classDeclaration) { - throw new Error('Failed to find class ' + this.qualifiedName); - } - this.classDeclaration = classDeclaration; + if(!modelFile) { + throw new IllegalModelException('Failed to find namespace ' + this.qualifiedName); } - else if(this.instanceId === null && this.variableName === null) { - // assume namespace - const modelFile = mm.getModelFile(this.qualifiedName); - if(!modelFile) { - throw new Error('Failed to find namespace ' + this.qualifiedName); - } + } else { + const modelFile = mm.getModelFile(ns); + + if(!modelFile) { + throw new IllegalModelException('Failed to find namespace ' + ns); } - else { - throw new Error('Failed to resolve ' + this.qualifiedName); + + const className = ModelUtil.getShortName(this.qualifiedName); + const classDeclaration = modelFile.getLocalType(className); + + if(!classDeclaration) { + throw new IllegalModelException('Failed to find class ' + this.qualifiedName); } + + this.classDeclaration = classDeclaration; } } diff --git a/packages/composer-common/lib/acl/parser.pegjs b/packages/composer-common/lib/acl/parser.pegjs index 7188268e95..a72df4bfc5 100644 --- a/packages/composer-common/lib/acl/parser.pegjs +++ b/packages/composer-common/lib/acl/parser.pegjs @@ -1424,8 +1424,12 @@ InstanceId return id; } +Glob + = '.**' + / '.*' + Binding - = qualifiedName:QualifiedName instanceId:InstanceId? + = qualifiedName:QualifiedName instanceId:InstanceId { return { type: "Binding", @@ -1436,17 +1440,18 @@ Binding } BindingNoInstance - = qualifiedName:QualifiedName + = qualifiedName:(QualifiedName Glob?) { return { type: "BindingNoInstance", - qualifiedName: qualifiedName, + qualifiedName: qualifiedName.join(''), location: location() }; } Noun = Binding + / BindingNoInstance NounNoInstance = BindingNoInstance @@ -1486,8 +1491,9 @@ AllVerb = 'ALL' Verbs = AllVerb / BasicVerbList Participant - = 'ANY' / - Binding + = 'ANY' + / Binding + / BindingNoInstance Predicate = "(" __ test:$Expression __ ")" __ diff --git a/packages/composer-common/lib/modelutil.js b/packages/composer-common/lib/modelutil.js index 9c02a645b5..c4dbda366d 100644 --- a/packages/composer-common/lib/modelutil.js +++ b/packages/composer-common/lib/modelutil.js @@ -44,15 +44,51 @@ class ModelUtil { } /** - * Returns true if the specified name is a wildcard. + * Returns true if the specified name is a wildcard * @param {string} fqn - the source string - * @return {boolean} true if the specified name is a wildcard. + * @return {boolean} true if the specified name is a wildcard * @private */ static isWildcardName(fqn) { return ModelUtil.getShortName(fqn) === '*'; } + /** + * Returns true if the specified name is a recusive wildcard + * @param {string} fqn - the source string + * @return {boolean} true if the specified name is a recusive wildcard + * @private + */ + static isRecursiveWildcardName(fqn) { + return ModelUtil.getShortName(fqn) === '**'; + } + + /** + * Returns true if a type matches the required fully qualified name. The required + * name may be a wildcard or recursive wildcard + * @param {Typed} type - the type to test + * @param {string} fqn - required fully qualified name + * @return {boolean} true if the specified type and namespace match + * @private + */ + static isMatchingType(type, fqn) { + let ns = ModelUtil.getNamespace(fqn); + let typeNS = type.getNamespace(); + + if (type.instanceOf(fqn)) { + // matching type or subtype + } else if (ModelUtil.isWildcardName(fqn) && typeNS === ns) { + // matching namespace + } else if (ModelUtil.isRecursiveWildcardName(fqn) && (typeNS + '.').startsWith(ns + '.')) { + // matching recursive namespace + } else { + // does not match + return false; + } + + return true; + } + /** * Returns the namespace for a the fully qualified name of a type * @param {string} fqn - the fully qualified identifier of a type @@ -76,13 +112,13 @@ class ModelUtil { /** * Returns true if the type is a primitive type - * @param {string} type - the name of the type + * @param {string} typeName - the name of the type * @return {boolean} - true if the type is a primitive * @private */ - static isPrimitiveType(type) { + static isPrimitiveType(typeName) { const primitiveTypes = ['Boolean', 'String', 'DateTime', 'Double', 'Integer', 'Long']; - return (primitiveTypes.indexOf(type) >= 0); + return (primitiveTypes.indexOf(typeName) >= 0); } /** diff --git a/packages/composer-common/messages/en.json b/packages/composer-common/messages/en.json index d0065c9aab..534800e1e7 100644 --- a/packages/composer-common/messages/en.json +++ b/packages/composer-common/messages/en.json @@ -56,7 +56,6 @@ "modelutil-getnamespace-nofnq": "FQN is invalid.", - "resourcevalidator-notresourceorconcept": "Model violation in instance {resourceId} class {classFQN} has value {invalidValue} expected a Resource or a Concept.", "resourcevalidator-notrelationship": "Model violation in instance {resourceId} class {classFQN} has value {invalidValue} expected a Relationship.", "resourcevalidator-fieldtypeviolation": "Model violation in instance {resourceId} field {propertyName} has value {value} ({typeOfValue}) expected type {fieldType}", diff --git a/packages/composer-common/package.json b/packages/composer-common/package.json index 677963e92e..252edd3bf9 100644 --- a/packages/composer-common/package.json +++ b/packages/composer-common/package.json @@ -1,6 +1,6 @@ { "name": "composer-common", - "version": "0.8.2", + "version": "0.9.0", "description": "Hyperledger Composer Common, code that is common across client, admin and runtime.", "engines": { "node": ">=6", diff --git a/packages/composer-common/test/acl/aclfile.js b/packages/composer-common/test/acl/aclfile.js index 290a1b9f32..00c1bf93f3 100644 --- a/packages/composer-common/test/acl/aclfile.js +++ b/packages/composer-common/test/acl/aclfile.js @@ -17,6 +17,8 @@ const AclFile = require('../../lib/acl/aclfile'); const parser = require('../../lib/acl/parser'); const ModelManager = require('../../lib/modelmanager'); +const IllegalModelException = require('../../lib/introspect/illegalmodelexception'); +const ParseException = require('../../lib/introspect/parseexception'); const fs = require('fs'); const path = require('path'); @@ -81,7 +83,7 @@ describe('AclFile', () => { describe('#constructor', () => { - it('should parse a rule correctly', () => { + it('should parse R1 rule correctly', () => { const aclContents = `rule R1 { description: "Fred can DELETE the car ABC123" participant: "org.acme.Driver#Fred" @@ -105,7 +107,7 @@ describe('AclFile', () => { r1.getDescription().should.equal('Fred can DELETE the car ABC123'); }); - it('should parse a rule correctly', () => { + it('should parse R2 rule correctly', () => { const aclContents = `rule R2 { description: "regulator with ID Bill can not update a Car if they own it" participant(r): "org.acme.Regulator#Bill" @@ -126,13 +128,12 @@ describe('AclFile', () => { r2.getParticipant().getVariableName().should.equal('r'); }); - it('should parse a rule correctly', () => { + it('should parse R3 rule correctly', () => { const aclContents = `rule R3 { - description: "Driver can change the ownership of a car that they own" - participant(d): "org.acme.Driver" - operation: UPDATE - resource(o): "org.acme.Car.owner" - condition: (o == d) + description: "regulators can perform all operations on Cars" + participant: "org.acme.Regulator" + operation: ALL + resource: "org.acme.Car" action: ALLOW }`; const aclFile = new AclFile('test.acl', modelManager, aclContents); @@ -140,20 +141,21 @@ describe('AclFile', () => { aclFile.getDefinitions().should.equal(aclContents); const r3 = aclFile.getAclRules()[0]; r3.getName().should.equal('R3'); - r3.getNoun().getFullyQualifiedName().should.equal('org.acme.Car.owner'); - r3.getVerbs().should.deep.equal(['UPDATE']); - r3.getParticipant().getFullyQualifiedName().should.equal('org.acme.Driver'); - r3.getParticipant().getVariableName().should.equal('d'); + r3.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); + r3.getVerbs().should.deep.equal(['ALL']); + r3.getParticipant().getFullyQualifiedName().should.equal('org.acme.Regulator'); + (r3.getParticipant().getInstanceIdentifier() === null).should.be.true; + (r3.getParticipant().getVariableName() === null).should.be.true; (r3.getTransaction() === null).should.be.true; - r3.getPredicate().getExpression().should.equal('o == d'); + r3.getPredicate().getExpression().should.equal('true'); r3.getAction().should.equal('ALLOW'); - r3.getDescription().should.equal('Driver can change the ownership of a car that they own'); + r3.getDescription().should.equal('regulators can perform all operations on Cars'); }); - it('should parse a rule correctly', () => { + it('should parse R4 rule correctly', () => { const aclContents = `rule R4 { - description: "regulators can perform all operations on Cars" - participant: "org.acme.Regulator" + description: "anyone in the org.acme namespace can perform all operations on Cars" + participant: "org.acme.*" operation: ALL resource: "org.acme.Car" action: ALLOW @@ -165,21 +167,21 @@ describe('AclFile', () => { r4.getName().should.equal('R4'); r4.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); r4.getVerbs().should.deep.equal(['ALL']); - r4.getParticipant().getFullyQualifiedName().should.equal('org.acme.Regulator'); + r4.getParticipant().getFullyQualifiedName().should.equal('org.acme.*'); (r4.getParticipant().getInstanceIdentifier() === null).should.be.true; (r4.getParticipant().getVariableName() === null).should.be.true; (r4.getTransaction() === null).should.be.true; r4.getPredicate().getExpression().should.equal('true'); r4.getAction().should.equal('ALLOW'); - r4.getDescription().should.equal('regulators can perform all operations on Cars'); + r4.getDescription().should.equal('anyone in the org.acme namespace can perform all operations on Cars'); }); - it('should parse a rule correctly', () => { + it('should parse R5 rule correctly', () => { const aclContents = `rule R5 { - description: "Everyone can read all resources in the org.acme namespace" - participant: "ANY" - operation: READ - resource: "org.acme" + description: "anyone under the org.acme namespace can perform all operations on Cars" + participant: "org.acme.**" + operation: ALL + resource: "org.acme.Car" action: ALLOW }`; const aclFile = new AclFile('test.acl', modelManager, aclContents); @@ -187,22 +189,23 @@ describe('AclFile', () => { aclFile.getDefinitions().should.equal(aclContents); const r5 = aclFile.getAclRules()[0]; r5.getName().should.equal('R5'); - r5.getNoun().getFullyQualifiedName().should.equal('org.acme'); - r5.getVerbs().should.deep.equal(['READ']); - (r5.getParticipant() === null).should.be.true; + r5.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); + r5.getVerbs().should.deep.equal(['ALL']); + r5.getParticipant().getFullyQualifiedName().should.equal('org.acme.**'); + (r5.getParticipant().getInstanceIdentifier() === null).should.be.true; + (r5.getParticipant().getVariableName() === null).should.be.true; (r5.getTransaction() === null).should.be.true; r5.getPredicate().getExpression().should.equal('true'); r5.getAction().should.equal('ALLOW'); - r5.getDescription().should.equal('Everyone can read all resources in the org.acme namespace'); + r5.getDescription().should.equal('anyone under the org.acme namespace can perform all operations on Cars'); }); - it('should parse a rule correctly', () => { + it('should parse R6 rule correctly', () => { const aclContents = `rule R6 { - description: "Drivers can do something in a org.acme.Transaction transaction" + description: "Everyone can read all resources in the org.acme namespace" participant: "ANY" - operation: ALL - resource: "org.acme.Car" - transaction: "org.acme.Transaction" + operation: READ + resource: "org.acme.*" action: ALLOW }`; const aclFile = new AclFile('test.acl', modelManager, aclContents); @@ -210,24 +213,21 @@ describe('AclFile', () => { aclFile.getDefinitions().should.equal(aclContents); const r6 = aclFile.getAclRules()[0]; r6.getName().should.equal('R6'); - r6.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); - r6.getVerbs().should.deep.equal(['ALL']); + r6.getNoun().getFullyQualifiedName().should.equal('org.acme.*'); + r6.getVerbs().should.deep.equal(['READ']); (r6.getParticipant() === null).should.be.true; - r6.getTransaction().getFullyQualifiedName().should.equal('org.acme.Transaction'); - (r6.getTransaction().getVariableName() === null).should.be.true; + (r6.getTransaction() === null).should.be.true; r6.getPredicate().getExpression().should.equal('true'); r6.getAction().should.equal('ALLOW'); - r6.getDescription().should.equal('Drivers can do something in a org.acme.Transaction transaction'); + r6.getDescription().should.equal('Everyone can read all resources in the org.acme namespace'); }); - it('should parse a rule correctly', () => { + it('should parse R7 rule correctly', () => { const aclContents = `rule R7 { - description: "Regulators can do something in a org.acme.Transaction transaction" + description: "Everyone can read all resources under the org.acme namespace" participant: "ANY" - operation: ALL - resource: "org.acme.Car" - transaction(tx): "org.acme.Transaction" - condition: (tx.asset.colour === 'blue') + operation: READ + resource: "org.acme.**" action: ALLOW }`; const aclFile = new AclFile('test.acl', modelManager, aclContents); @@ -235,19 +235,114 @@ describe('AclFile', () => { aclFile.getDefinitions().should.equal(aclContents); const r7 = aclFile.getAclRules()[0]; r7.getName().should.equal('R7'); - r7.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); - r7.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); - r7.getVerbs().should.deep.equal(['ALL']); + r7.getNoun().getFullyQualifiedName().should.equal('org.acme.**'); + r7.getVerbs().should.deep.equal(['READ']); (r7.getParticipant() === null).should.be.true; - r7.getTransaction().getFullyQualifiedName().should.equal('org.acme.Transaction'); - r7.getTransaction().getVariableName().should.equal('tx'); - r7.getPredicate().getExpression().should.equal('tx.asset.colour === \'blue\''); + (r7.getTransaction() === null).should.be.true; + r7.getPredicate().getExpression().should.equal('true'); r7.getAction().should.equal('ALLOW'); - r7.getDescription().should.equal('Regulators can do something in a org.acme.Transaction transaction'); + r7.getDescription().should.equal('Everyone can read all resources under the org.acme namespace'); }); - it('should parse a rule correctly', () => { + it('should parse R8 rule correctly', () => { const aclContents = `rule R8 { + description: "Drivers can do something in a org.acme.Transaction transaction" + participant: "ANY" + operation: ALL + resource: "org.acme.Car" + transaction: "org.acme.Transaction" + action: ALLOW + }`; + const aclFile = new AclFile('test.acl', modelManager, aclContents); + aclFile.getAclRules().length.should.equal(1); + aclFile.getDefinitions().should.equal(aclContents); + const r8 = aclFile.getAclRules()[0]; + r8.getName().should.equal('R8'); + r8.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); + r8.getVerbs().should.deep.equal(['ALL']); + (r8.getParticipant() === null).should.be.true; + r8.getTransaction().getFullyQualifiedName().should.equal('org.acme.Transaction'); + (r8.getTransaction().getVariableName() === null).should.be.true; + r8.getPredicate().getExpression().should.equal('true'); + r8.getAction().should.equal('ALLOW'); + r8.getDescription().should.equal('Drivers can do something in a org.acme.Transaction transaction'); + }); + + it('should parse R9 rule correctly', () => { + const aclContents = `rule R9 { + description: "Drivers can do something with transactions in the org.acme namespace" + participant: "ANY" + operation: ALL + resource: "org.acme.Car" + transaction: "org.acme.*" + action: ALLOW + }`; + const aclFile = new AclFile('test.acl', modelManager, aclContents); + aclFile.getAclRules().length.should.equal(1); + aclFile.getDefinitions().should.equal(aclContents); + const r9 = aclFile.getAclRules()[0]; + r9.getName().should.equal('R9'); + r9.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); + r9.getVerbs().should.deep.equal(['ALL']); + (r9.getParticipant() === null).should.be.true; + r9.getTransaction().getFullyQualifiedName().should.equal('org.acme.*'); + (r9.getTransaction().getVariableName() === null).should.be.true; + r9.getPredicate().getExpression().should.equal('true'); + r9.getAction().should.equal('ALLOW'); + r9.getDescription().should.equal('Drivers can do something with transactions in the org.acme namespace'); + }); + + it('should parse R10 rule correctly', () => { + const aclContents = `rule R10 { + description: "Drivers can do something with transactions under the org.acme namespace" + participant: "ANY" + operation: ALL + resource: "org.acme.Car" + transaction: "org.acme.**" + action: ALLOW + }`; + const aclFile = new AclFile('test.acl', modelManager, aclContents); + aclFile.getAclRules().length.should.equal(1); + aclFile.getDefinitions().should.equal(aclContents); + const r10 = aclFile.getAclRules()[0]; + r10.getName().should.equal('R10'); + r10.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); + r10.getVerbs().should.deep.equal(['ALL']); + (r10.getParticipant() === null).should.be.true; + r10.getTransaction().getFullyQualifiedName().should.equal('org.acme.**'); + (r10.getTransaction().getVariableName() === null).should.be.true; + r10.getPredicate().getExpression().should.equal('true'); + r10.getAction().should.equal('ALLOW'); + r10.getDescription().should.equal('Drivers can do something with transactions under the org.acme namespace'); + }); + + it('should parse R11 rule correctly', () => { + const aclContents = `rule R11 { + description: "Regulators can do something in a org.acme.Transaction transaction" + participant: "ANY" + operation: ALL + resource: "org.acme.Car" + transaction(tx): "org.acme.Transaction" + condition: (tx.asset.colour === 'blue') + action: ALLOW + }`; + const aclFile = new AclFile('test.acl', modelManager, aclContents); + aclFile.getAclRules().length.should.equal(1); + aclFile.getDefinitions().should.equal(aclContents); + const r11 = aclFile.getAclRules()[0]; + r11.getName().should.equal('R11'); + r11.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); + r11.getVerbs().should.deep.equal(['ALL']); + (r11.getParticipant() === null).should.be.true; + r11.getTransaction().getFullyQualifiedName().should.equal('org.acme.Transaction'); + r11.getTransaction().getVariableName().should.equal('tx'); + r11.getPredicate().getExpression().should.equal('tx.asset.colour === \'blue\''); + r11.getAction().should.equal('ALLOW'); + r11.getDescription().should.equal('Regulators can do something in a org.acme.Transaction transaction'); + }); + + it('should parse R12 rule correctly', () => { + const aclContents = `rule R12 { description: "Fred can CREATE, READ, and UPDATE the car ABC123" participant: "org.acme.Driver#Fred" operation: CREATE, READ, UPDATE @@ -257,12 +352,20 @@ describe('AclFile', () => { const aclFile = new AclFile('test.acl', modelManager, aclContents); aclFile.getAclRules().length.should.equal(1); aclFile.getDefinitions().should.equal(aclContents); - const r8 = aclFile.getAclRules()[0]; - r8.getVerbs().should.deep.equal(['CREATE', 'READ', 'UPDATE']); + const r12 = aclFile.getAclRules()[0]; + r12.getName().should.equal('R12'); + r12.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); + r12.getNoun().getInstanceIdentifier().should.equal('ABC123'); + r12.getVerbs().should.deep.equal(['CREATE', 'READ', 'UPDATE']); + r12.getParticipant().getFullyQualifiedName().should.equal('org.acme.Driver'); + r12.getParticipant().getInstanceIdentifier().should.equal('Fred'); + (r12.getParticipant().getVariableName() === null).should.be.true; + r12.getAction().should.equal('ALLOW'); + r12.getDescription().should.equal('Fred can CREATE, READ, and UPDATE the car ABC123'); }); - it('should parse a rule correctly', () => { - const aclContents = `rule R9 { + it('should parse R13 rule correctly', () => { + const aclContents = `rule R13 { description: "regulator with ID Bill can not update or delete a Car if they own it" participant(r): "org.acme.Regulator#Bill" operation: UPDATE, DELETE @@ -273,12 +376,18 @@ describe('AclFile', () => { const aclFile = new AclFile('test.acl', modelManager, aclContents); aclFile.getAclRules().length.should.equal(1); aclFile.getDefinitions().should.equal(aclContents); - const r9 = aclFile.getAclRules()[0]; - r9.getVerbs().should.deep.equal(['UPDATE', 'DELETE']); + const r13 = aclFile.getAclRules()[0]; + r13.getName().should.equal('R13'); + r13.getNoun().getFullyQualifiedName().should.equal('org.acme.Car'); + (r13.getNoun().getInstanceIdentifier() === null).should.be.true; + r13.getParticipant().getFullyQualifiedName().should.equal('org.acme.Regulator'); + r13.getParticipant().getInstanceIdentifier().should.equal('Bill'); + r13.getParticipant().getVariableName().should.equal('r'); + r13.getVerbs().should.deep.equal(['UPDATE', 'DELETE']); }); it('should fail to parse a rule with ALL and another verb', () => { - const aclContents = `rule R9 { + const aclContents = `rule A { description: "regulator with ID Bill can not update or delete a Car if they own it" participant(r): "org.acme.Regulator#Bill" operation: ALL, DELETE @@ -288,31 +397,45 @@ describe('AclFile', () => { }`; (() => { new AclFile('test.acl', modelManager, aclContents); - }).should.throw(/Expected.*but.*found/); + }).should.throw(ParseException, /Expected.*but.*found/); }); - it('should fail to parse a rule with duplicate verbs', () => { - const aclContents = `rule R9 { + it('should fail to parse a rule with a glob and instance ID', () => { + const aclContents = `rule B { description: "regulator with ID Bill can not update or delete a Car if they own it" - participant(r): "org.acme.Regulator#Bill" - operation: DELETE, READ, DELETE + participant(r): "org.acme.Regulator.*#Bill" + operation: UPDATE, DELETE resource(c): "org.acme.Car" condition: (c.owner == r) action: DENY }`; - const aclFile = new AclFile('test.acl', modelManager, aclContents); (() => { - aclFile.validate(); - }).should.throw(/has been specified more than once/); + new AclFile('test.acl', modelManager, aclContents); + }).should.throw(ParseException, /Expected.*but.*found/); + }); + + it('should fail to parse a rule with a recursive glob and instance ID', () => { + const aclContents = `rule C { + description: "regulator with ID Bill can not update or delete a Car if they own it" + participant(r): "org.acme.Regulator.**#Bill" + operation: UPDATE, DELETE + resource(c): "org.acme.Car" + condition: (c.owner == r) + action: DENY + }`; + (() => { + new AclFile('test.acl', modelManager, aclContents); + }).should.throw(ParseException, /Expected.*but.*found/); }); + it('should parse correctly and preserve order', () => { const aclFile = new AclFile('test.acl', modelManager, testAcl); - aclFile.getAclRules().length.should.equal(9); + aclFile.getAclRules().length.should.equal(13); aclFile.getDefinitions().should.equal(testAcl); aclFile.getAclRules().map((aclRule) => { return aclRule.getName(); - }).should.deep.equal(['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8' , 'R9']); + }).should.deep.equal(['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8' , 'R9', 'R10', 'R11', 'R12', 'R13']); }); }); @@ -329,7 +452,7 @@ describe('AclFile', () => { description: "some rule" participant: "ANY" operation: ALL - resource: "org.acme" + resource: "org.acme.*" action: ALLOW } @@ -337,7 +460,7 @@ describe('AclFile', () => { description: "some rule" participant: "ANY" operation: ALL - resource: "org.acme" + resource: "org.acme.*" action: ALLOW }`; const aclFile = new AclFile('test.acl', modelManager, aclContents); @@ -346,6 +469,50 @@ describe('AclFile', () => { }).should.throw(/Found two or more ACL rules with the name/); }); + it('should fail to validate a rule with duplicate verbs', () => { + const aclContents = `rule A { + description: "regulator with ID Bill can not update or delete a Car if they own it" + participant(r): "org.acme.Regulator#Bill" + operation: DELETE, READ, DELETE + resource(c): "org.acme.Car" + condition: (c.owner == r) + action: DENY + }`; + const aclFile = new AclFile('test.acl', modelManager, aclContents); + (() => { + aclFile.validate(); + }).should.throw(/has been specified more than once/); + }); + + it('should fail to validate a rule when participant is not a participant', () => { + const aclContents = `rule B { + description: "org.acme.Car is an asset, not a particpant" + participant: "org.acme.Car" + operation: READ + resource: "org.acme.Car" + action: ALLOW + }`; + const aclFile = new AclFile('test.acl', modelManager, aclContents); + (() => { + aclFile.validate(); + }).should.throw(IllegalModelException, /The participant.*must be a participant/); + }); + + it('should fail to validate a rule when transaction is not a transaction', () => { + const aclContents = `rule D { + description: "org.acme.Car is an asset, not a transaction" + participant: "ANY" + operation: ALL + resource: "org.acme.*" + transaction: "org.acme.Car" + action: ALLOW + }`; + const aclFile = new AclFile('test.acl', modelManager, aclContents); + (() => { + aclFile.validate(); + }).should.throw(IllegalModelException, /The transaction.*must be a transaction/); + }); + }); describe('#accept', () => { diff --git a/packages/composer-common/test/acl/aclrule.js b/packages/composer-common/test/acl/aclrule.js index a513fde2f5..6a4208ebae 100644 --- a/packages/composer-common/test/acl/aclrule.js +++ b/packages/composer-common/test/acl/aclrule.js @@ -18,6 +18,8 @@ const AclRule = require('../../lib/acl/aclrule'); const AclFile = require('../../lib/acl/aclfile'); const ModelManager = require('../../lib/modelmanager'); const ModelFile = require('../../lib/introspect/modelfile'); +const AssetDeclaration = require('../../lib/introspect/assetdeclaration'); +const ParticipantDeclaration = require('../../lib/introspect/participantdeclaration'); const should = require('chai').should(); const sinon = require('sinon'); @@ -28,6 +30,8 @@ describe('AclRule', () => { let aclFile; let mockModelManager; let mockModelFile; + let mockAssetDeclaration; + let mockParticipantDeclaration; let sandbox; const ast = { @@ -65,9 +69,11 @@ describe('AclRule', () => { mockModelManager = sinon.createStubInstance(ModelManager); aclFile.getModelManager.returns(mockModelManager); mockModelFile = sinon.createStubInstance(ModelFile); + mockAssetDeclaration = sinon.createStubInstance(AssetDeclaration); + mockParticipantDeclaration = sinon.createStubInstance(ParticipantDeclaration); mockModelManager.getModelFile.withArgs('org.acme').returns(mockModelFile); - mockModelFile.getLocalType.withArgs('Car').returns('fake'); - mockModelFile.getLocalType.withArgs('Driver').returns('fake'); + mockModelFile.getLocalType.withArgs('Car').returns(mockAssetDeclaration); + mockModelFile.getLocalType.withArgs('Driver').returns(mockParticipantDeclaration); sandbox = sinon.sandbox.create(); }); diff --git a/packages/composer-common/test/acl/modelbinding.js b/packages/composer-common/test/acl/modelbinding.js index 53944f11ce..b0127f9a5c 100644 --- a/packages/composer-common/test/acl/modelbinding.js +++ b/packages/composer-common/test/acl/modelbinding.js @@ -30,18 +30,16 @@ describe('ModelBinding', () => { let modelManager; let sandbox; - const namespaceAst = {'type':'Binding','qualifiedName':'org.acme'}; + const namespaceAst = {'type':'Binding','qualifiedName':'org.acme.*'}; + const recursiveNamespaceAst = {'type':'Binding','qualifiedName':'org.**'}; const classAst = {'type':'Binding','qualifiedName':'org.acme.Car'}; const classWithIdentifierAst = {'type':'Binding','qualifiedName':'org.acme.Car','instanceId':'ABC123'}; - const propertyAst = {'type':'Binding','qualifiedName':'org.acme.Car.assetId'}; - const propertyWithIdentifierAst = {'type':'Binding','qualifiedName':'org.acme.Car.assetId','instanceId':'ABC123'}; const variableAst = {'type':'Identifier','name':'dan'}; const missingClass = {'type':'Binding','qualifiedName':'org.acme.Missing','instanceId':'ABC123'}; - const missingNamespace = {'type':'Binding','qualifiedName':'org.missing.Missing'}; - const missingClassWithProperty = {'type':'Binding','qualifiedName':'org.acme.Missing.missing','instanceId':'ABC123'}; - const missingProperty = {'type':'Binding','qualifiedName':'org.acme.Car.missing','instanceId':'ABC123'}; - const missing = {'type':'Binding','qualifiedName':'org.missing.Missing','instanceId':'ABC123'}; + const missingNamespace = {'type':'Binding','qualifiedName':'org.missing.Missing.*'}; + const missingRecursiveNamespace = {'type':'Binding','qualifiedName':'org.missing.Missing.**'}; + const missing = {'type':'Binding','qualifiedName':'org.missing.Missing.*','instanceId':'ABC123'}; beforeEach(() => { aclFile = sinon.createStubInstance(AclFile); @@ -91,7 +89,13 @@ describe('ModelBinding', () => { it('should validate correct contents for a namespace reference', () => { modelBinding = new ModelBinding( aclRule, namespaceAst ); modelBinding.validate(); - modelBinding.toString().should.equal('ModelBinding org.acme'); + modelBinding.toString().should.equal('ModelBinding org.acme.*'); + }); + + it('should validate correct contents for a recursive namespace reference', () => { + modelBinding = new ModelBinding( aclRule, recursiveNamespaceAst ); + modelBinding.validate(); + modelBinding.toString().should.equal('ModelBinding org.**'); }); it('should validate correct contents for a class reference', () => { @@ -106,18 +110,6 @@ describe('ModelBinding', () => { modelBinding.toString().should.equal('ModelBinding org.acme.Car#ABC123'); }); - it('should validate correct contents for a property reference', () => { - modelBinding = new ModelBinding( aclRule, propertyAst ); - modelBinding.validate(); - modelBinding.toString().should.equal('ModelBinding org.acme.Car.assetId'); - }); - - it('should validate correct contents for a property reference with an identifier', () => { - modelBinding = new ModelBinding( aclRule, propertyWithIdentifierAst ); - modelBinding.validate(); - modelBinding.toString().should.equal('ModelBinding org.acme.Car.assetId#ABC123'); - }); - it('should validate correct contents for a class reference with a variable binding', () => { modelBinding = new ModelBinding( aclRule, classAst, variableAst ); modelBinding.validate(); @@ -130,18 +122,6 @@ describe('ModelBinding', () => { modelBinding.toString().should.equal('ModelBinding org.acme.Car#ABC123:dan'); }); - it('should validate correct contents for a property reference with a variable binding', () => { - modelBinding = new ModelBinding( aclRule, propertyAst, variableAst ); - modelBinding.validate(); - modelBinding.toString().should.equal('ModelBinding org.acme.Car.assetId:dan'); - }); - - it('should validate correct contents for a property reference with an identifier and with a variable binding', () => { - modelBinding = new ModelBinding( aclRule, propertyWithIdentifierAst, variableAst ); - modelBinding.validate(); - modelBinding.toString().should.equal('ModelBinding org.acme.Car.assetId#ABC123:dan'); - }); - it('should detect reference to missing class', () => { (() => { modelBinding = new ModelBinding( aclRule, missingClass ); @@ -156,25 +136,18 @@ describe('ModelBinding', () => { }).should.throw(/Failed to find namespace org.missing.Missing/); }); - it('should detect reference to missing namespace with variable name', () => { - (() => { - modelBinding = new ModelBinding( aclRule, missing ); - modelBinding.validate(); - }).should.throw(/Failed to resolve org.missing.Missing/); - }); - - it('should detect reference to missing property', () => { + it('should detect reference to missing recursive namespace', () => { (() => { - modelBinding = new ModelBinding( aclRule, missingProperty ); + modelBinding = new ModelBinding( aclRule, missingRecursiveNamespace ); modelBinding.validate(); - }).should.throw(/Failed to find property org.acme.Car.missing/); + }).should.throw(/Failed to find namespace org.missing.Missing/); }); - it('should detect reference to missing class with property', () => { + it('should detect reference to missing namespace with variable name', () => { (() => { - modelBinding = new ModelBinding( aclRule, missingClassWithProperty ); + modelBinding = new ModelBinding( aclRule, missing ); modelBinding.validate(); - }).should.throw(/Failed to find class org.acme.Missing/); + }).should.throw(/Failed to find namespace org.missing.Missing/); }); }); @@ -200,14 +173,14 @@ describe('ModelBinding', () => { should.equal(modelBinding.getClassDeclaration(), null); }); - it('should return the class declaration for a class binding', () => { - modelBinding = new ModelBinding( aclRule, classAst ); + it('should return null for a recursive namespace binding', () => { + modelBinding = new ModelBinding( aclRule, recursiveNamespaceAst ); modelBinding.validate(); - modelBinding.getClassDeclaration().getFullyQualifiedName().should.equal('org.acme.Car'); + should.equal(modelBinding.getClassDeclaration(), null); }); - it('should return the class declaration for a property binding', () => { - modelBinding = new ModelBinding( aclRule, propertyAst ); + it('should return the class declaration for a class binding', () => { + modelBinding = new ModelBinding( aclRule, classAst ); modelBinding.validate(); modelBinding.getClassDeclaration().getFullyQualifiedName().should.equal('org.acme.Car'); }); diff --git a/packages/composer-common/test/acl/test.acl b/packages/composer-common/test/acl/test.acl index 330e056ae9..b0e86b091f 100644 --- a/packages/composer-common/test/acl/test.acl +++ b/packages/composer-common/test/acl/test.acl @@ -16,31 +16,46 @@ rule R2 { } rule R3 { - description: "Driver can change the ownership of a car that they own" - participant(d): "org.acme.Driver" - operation: UPDATE - resource(o): "org.acme.Car.owner" - condition: (o == d) + description: "regulators can perform all operations on Cars" + participant: "org.acme.Regulator" + operation: ALL + resource: "org.acme.Car" action: ALLOW } rule R4 { - description: "regulators can perform all operations on Cars" - participant: "org.acme.Regulator" + description: "anyone in the org.acme namespace can perform all operations on Cars" + participant: "org.acme.*" operation: ALL resource: "org.acme.Car" action: ALLOW } rule R5 { + description: "anyone under the org.acme namespace can perform all operations on Cars" + participant: "org.acme.**" + operation: ALL + resource: "org.acme.Car" + action: ALLOW +} + +rule R6 { description: "Everyone can read all resources in the org.acme namespace" participant: "ANY" operation: READ - resource: "org.acme" + resource: "org.acme.*" action: ALLOW } -rule R6 { +rule R7 { + description: "Everyone can read all resources under the org.acme namespace" + participant: "ANY" + operation: READ + resource: "org.acme.**" + action: ALLOW +} + +rule R8 { description: "Drivers can do something in a org.acme.Transaction transaction" participant: "ANY" operation: ALL @@ -49,7 +64,25 @@ rule R6 { action: ALLOW } -rule R7 { +rule R9 { + description: "Drivers can do something with transactions in the org.acme namespace" + participant: "ANY" + operation: ALL + resource: "org.acme.Car" + transaction: "org.acme.*" + action: ALLOW +} + +rule R10 { + description: "Drivers can do something with transactions under the org.acme namespace" + participant: "ANY" + operation: ALL + resource: "org.acme.Car" + transaction: "org.acme.**" + action: ALLOW +} + +rule R11 { description: "Regulators can do something in a org.acme.Transaction transaction" participant: "ANY" operation: ALL @@ -59,7 +92,7 @@ rule R7 { action: ALLOW } -rule R8 { +rule R12 { description: "Fred can CREATE, READ, and UPDATE the car ABC123" participant: "org.acme.Driver#Fred" operation: CREATE, READ, UPDATE @@ -67,7 +100,7 @@ rule R8 { action: ALLOW } -rule R9 { +rule R13 { description: "regulator with ID Bill can not update or delete a Car if they own it" participant(r): "org.acme.Regulator#Bill" operation: UPDATE, DELETE diff --git a/packages/composer-common/test/businessnetworkdefinition.js b/packages/composer-common/test/businessnetworkdefinition.js index 6ea59e80dc..7fff0e5c6a 100644 --- a/packages/composer-common/test/businessnetworkdefinition.js +++ b/packages/composer-common/test/businessnetworkdefinition.js @@ -100,7 +100,7 @@ describe('BusinessNetworkDefinition', () => { businessNetwork.getMetadata().getPackageJson().customKey.should.equal('custom value'); Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(3); Object.keys(businessNetwork.scriptManager.scripts).should.have.length(2); - businessNetwork.aclManager.getAclRules().should.have.length(5); + businessNetwork.aclManager.getAclRules().should.have.length(4); const intro = businessNetwork.getIntrospector(); intro.getClassDeclarations().length.should.equal(25); diff --git a/packages/composer-common/test/data/zip/test-archive-dotfolders/permissions.acl b/packages/composer-common/test/data/zip/test-archive-dotfolders/permissions.acl index 3ba57239ef..25893994a9 100644 --- a/packages/composer-common/test/data/zip/test-archive-dotfolders/permissions.acl +++ b/packages/composer-common/test/data/zip/test-archive-dotfolders/permissions.acl @@ -17,15 +17,6 @@ rule R2 { } rule R3 { - description: "Driver can change the ownership of a car that they own" - participant(d): "concerto.Participant" - operation: UPDATE - resource(o): "org.acme.Vehicle.owner" - condition: (o == d) - action: ALLOW -} - -rule R4 { description: "regulators can perform all operations on Cars" participant: "org.acme.Regulator" operation: ALL @@ -33,10 +24,10 @@ rule R4 { action: ALLOW } -rule R5 { +rule R4 { description: "Everyone can read all resources in the org.acme namespace" participant: "ANY" operation: READ - resource: "org.acme" + resource: "org.acme.*" action: ALLOW } \ No newline at end of file diff --git a/packages/composer-common/test/data/zip/test-archive.zip b/packages/composer-common/test/data/zip/test-archive.zip index 0c18da48ed..4b4b44949c 100644 Binary files a/packages/composer-common/test/data/zip/test-archive.zip and b/packages/composer-common/test/data/zip/test-archive.zip differ diff --git a/packages/composer-common/test/data/zip/test-archive/permissions.acl b/packages/composer-common/test/data/zip/test-archive/permissions.acl index 3ba57239ef..25893994a9 100644 --- a/packages/composer-common/test/data/zip/test-archive/permissions.acl +++ b/packages/composer-common/test/data/zip/test-archive/permissions.acl @@ -17,15 +17,6 @@ rule R2 { } rule R3 { - description: "Driver can change the ownership of a car that they own" - participant(d): "concerto.Participant" - operation: UPDATE - resource(o): "org.acme.Vehicle.owner" - condition: (o == d) - action: ALLOW -} - -rule R4 { description: "regulators can perform all operations on Cars" participant: "org.acme.Regulator" operation: ALL @@ -33,10 +24,10 @@ rule R4 { action: ALLOW } -rule R5 { +rule R4 { description: "Everyone can read all resources in the org.acme namespace" participant: "ANY" operation: READ - resource: "org.acme" + resource: "org.acme.*" action: ALLOW } \ No newline at end of file diff --git a/packages/composer-common/test/modelutil.js b/packages/composer-common/test/modelutil.js index bbf01f96a4..3a75b64952 100644 --- a/packages/composer-common/test/modelutil.js +++ b/packages/composer-common/test/modelutil.js @@ -14,8 +14,10 @@ 'use strict'; +const ClassDeclaration = require('../lib/introspect/classdeclaration'); const ModelFile = require('../lib/introspect/modelfile'); const Property = require('../lib/introspect/property'); +const Typed = require('../lib/model/identifiable'); const ModelManager = require('../lib/modelmanager'); const ModelUtil = require('../lib/modelutil'); @@ -75,6 +77,83 @@ describe('ModelUtil', function () { ModelUtil.isWildcardName('org.acme.baz.*').should.be.true; }); + it('should return false for a fully qualified name with a recursive wildcard', () => { + ModelUtil.isWildcardName('org.acme.baz.**').should.be.false; + }); + + }); + + describe('#isRecursiveWildcardName', () => { + + it('should return false for a name without a recursive wildcard', () => { + ModelUtil.isRecursiveWildcardName('Foo').should.be.false; + }); + + it('should return true for a name with a recursive wildcard', () => { + ModelUtil.isRecursiveWildcardName('**').should.be.true; + }); + + it('should return false for a fully qualified name without a recursive wildcard', () => { + ModelUtil.isRecursiveWildcardName('org.acme.baz.Foo').should.be.false; + }); + + it('should return true for a fully qualified name with a recursive wildcard', () => { + ModelUtil.isRecursiveWildcardName('org.acme.baz.**').should.be.true; + }); + + it('should return false for a fully qualified name with a wildcard', () => { + ModelUtil.isRecursiveWildcardName('org.acme.baz.*').should.be.false; + }); + + }); + + describe('#isMatchingType', () => { + + let mockModelManager; + let mockModelFile; + let mockClassDeclaration; + let type; + + beforeEach(() => { + mockModelManager = sinon.createStubInstance(ModelManager); + mockModelFile = sinon.createStubInstance(ModelFile); + mockClassDeclaration = sinon.createStubInstance(ClassDeclaration); + + mockModelManager.getModelFile.returns(mockModelFile); + mockModelFile.getType.returns(mockClassDeclaration); + mockClassDeclaration.getFullyQualifiedName.returns('org.acme.baz.Foo'); + mockClassDeclaration.getAllSuperTypeDeclarations.returns([]); + + type = new Typed(mockModelManager, 'org.acme.baz', 'Foo' ); + }); + + it('should return true for an exact match', () => { + ModelUtil.isMatchingType(type, 'org.acme.baz.Foo').should.be.true; + }); + + it('should return false for a non-match', () => { + ModelUtil.isMatchingType(type, 'org.acme.baz.Bar').should.be.false; + }); + + it('should return true for a wildcard namespace match', () => { + ModelUtil.isMatchingType(type, 'org.acme.baz.*').should.be.true; + }); + + it('should return false for a non-matching wildcard namespace', () => { + ModelUtil.isMatchingType(type, 'org.doge.baz.*').should.be.false; + }); + + it('should return false for an ancestor namespace wildcard match', () => { + ModelUtil.isMatchingType(type, 'org.acme.*').should.be.false; + }); + + it('should return true for a recursive wildcard match', () => { + ModelUtil.isMatchingType(type, 'org.acme.**').should.be.true; + }); + + it('should return false for a non-matching recursive wildcard', () => { + ModelUtil.isMatchingType(type, 'org.ac.**').should.be.false; + }); }); describe('#getNamespace', function() { diff --git a/packages/composer-connector-embedded/package.json b/packages/composer-connector-embedded/package.json index fa1fb9ca8e..650fccbb27 100644 --- a/packages/composer-connector-embedded/package.json +++ b/packages/composer-connector-embedded/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-embedded", - "version": "0.8.2", + "version": "0.9.0", "description": "The embedded client connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -52,9 +52,9 @@ "watchify": "^3.7.0" }, "dependencies": { - "composer-common": "^0.8.2", - "composer-runtime": "^0.8.2", - "composer-runtime-embedded": "^0.8.2" + "composer-common": "^0.9.0", + "composer-runtime": "^0.9.0", + "composer-runtime-embedded": "^0.9.0" }, "nyc": { "exclude": [ diff --git a/packages/composer-connector-hlf/package.json b/packages/composer-connector-hlf/package.json index 90ae5fa91e..70dd6fed9b 100644 --- a/packages/composer-connector-hlf/package.json +++ b/packages/composer-connector-hlf/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-hlf", - "version": "0.8.2", + "version": "0.9.0", "description": "The Hyperledger Fabric Client connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -40,8 +40,8 @@ "logError": true }, "dependencies": { - "composer-common": "^0.8.2", - "composer-runtime-hlf": "^0.8.2", + "composer-common": "^0.9.0", + "composer-runtime-hlf": "^0.9.0", "fs-extra": "^1.0.0", "hfc": "^0.6.5", "semver": "^5.3.0", diff --git a/packages/composer-connector-hlfv1/package.json b/packages/composer-connector-hlfv1/package.json index fc68d8607a..512926747d 100644 --- a/packages/composer-connector-hlfv1/package.json +++ b/packages/composer-connector-hlfv1/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-hlfv1", - "version": "0.8.2", + "version": "0.9.0", "description": "The Hyperledger Fabric v1.x Client connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -40,8 +40,8 @@ "logError": true }, "dependencies": { - "composer-common": "^0.8.2", - "composer-runtime-hlfv1": "^0.8.2", + "composer-common": "^0.9.0", + "composer-runtime-hlfv1": "^0.9.0", "fabric-ca-client": "1.0.0-beta", "fabric-client": "1.0.0-beta", "fs-extra": "^1.0.0", diff --git a/packages/composer-connector-proxy/package.json b/packages/composer-connector-proxy/package.json index d68dd471b9..faa4a61622 100644 --- a/packages/composer-connector-proxy/package.json +++ b/packages/composer-connector-proxy/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-proxy", - "version": "0.8.2", + "version": "0.9.0", "description": "The proxying client connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -54,7 +54,7 @@ "watchify": "^3.7.0" }, "dependencies": { - "composer-common": "^0.8.2", + "composer-common": "^0.9.0", "socket.io-client": "^1.7.3" }, "nyc": { diff --git a/packages/composer-connector-server/package.json b/packages/composer-connector-server/package.json index ab8f7498c8..3b633a98c9 100644 --- a/packages/composer-connector-server/package.json +++ b/packages/composer-connector-server/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-server", - "version": "0.8.2", + "version": "0.9.0", "description": "The remote connector server for Hyperledger Composer", "engines": { "node": ">=6", @@ -44,10 +44,10 @@ "logError": true }, "dependencies": { - "composer-common": "^0.8.2", - "composer-connector-embedded": "^0.8.2", - "composer-connector-hlf": "^0.8.2", - "composer-connector-hlfv1": "^0.8.2", + "composer-common": "^0.9.0", + "composer-connector-embedded": "^0.9.0", + "composer-connector-hlf": "^0.9.0", + "composer-connector-hlfv1": "^0.9.0", "serializerr": "^1.0.3", "socket.io": "^1.7.3", "uuid": "^3.0.1", diff --git a/packages/composer-connector-web/package.json b/packages/composer-connector-web/package.json index 94236dba63..1eb056d00b 100644 --- a/packages/composer-connector-web/package.json +++ b/packages/composer-connector-web/package.json @@ -1,6 +1,6 @@ { "name": "composer-connector-web", - "version": "0.8.2", + "version": "0.9.0", "description": "The web client connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -62,9 +62,9 @@ "watchify": "^3.7.0" }, "dependencies": { - "composer-common": "^0.8.2", - "composer-runtime": "^0.8.2", - "composer-runtime-web": "^0.8.2", + "composer-common": "^0.9.0", + "composer-runtime": "^0.9.0", + "composer-runtime-web": "^0.9.0", "uuid": "^3.0.1" } } diff --git a/packages/composer-cucumber-steps/features/basic-sample-network.bna b/packages/composer-cucumber-steps/features/basic-sample-network.bna index cf9701b011..8d7797b7bd 100644 Binary files a/packages/composer-cucumber-steps/features/basic-sample-network.bna and b/packages/composer-cucumber-steps/features/basic-sample-network.bna differ diff --git a/packages/composer-cucumber-steps/features/basic-sample-network/README.md b/packages/composer-cucumber-steps/features/basic-sample-network/README.md index 976af21b8c..2d9b2af245 100644 --- a/packages/composer-cucumber-steps/features/basic-sample-network/README.md +++ b/packages/composer-cucumber-steps/features/basic-sample-network/README.md @@ -2,16 +2,16 @@ This is the "Hello World" of Hyperledger Composer samples. -This sample defines a business network composed of a single asset type (`SampleAsset`), a single participant type (`SampleParticipant`), and a single transaction type (`SampleTransaction`). +This sample defines a business network composed of a single asset type (`SampleAsset`), a single participant type (`SampleParticipant`), a single transaction type (`SampleTransaction`), and a single event type (`SampleEvent`). -`SampleAssets` are owned by a `SampleParticipant`, and the value property on a `SampleAsset` can be modified by submitting a `SampleTransaction`. +`SampleAssets` are owned by a `SampleParticipant`, and the value property on a `SampleAsset` can be modified by submitting a `SampleTransaction`. The `SampleTransaction` emits a `SampleEvent` that notifies applications of the old and new values for each modified `SampleAsset`. To get started inside Hyperledger Composer you can click the Test tab and create instances of `SampleAsset` and `SampleParticipant`. Make sure that the owner property on the `SampleAsset` refers to a `SampleParticipant` that you have created. -You can then submit a `SampleTransaction`, making sure that the asset property refers to an asset that you created earlier. After the transaction has been processed you should see that the value property on the asset has been modified. +You can then submit a `SampleTransaction`, making sure that the asset property refers to an asset that you created earlier. After the transaction has been processed you should see that the value property on the asset has been modified, and that a `SampleEvent` has been emitted. -The logic for updating the asset when a `SampleTransaction` is processed is written in `logic.js`. +The logic for updating the asset when a `SampleTransaction` is processed is written in `sample.js`. -Don't forget you can import more advanced samples into Hyperledger Composer using the Import/Replace button. +Don't forget that you can import more advanced samples into Hyperledger Composer using the Import/Replace button. Have fun learning Hyperledger Composer! \ No newline at end of file diff --git a/packages/composer-cucumber-steps/features/basic-sample-network/lib/logic.js b/packages/composer-cucumber-steps/features/basic-sample-network/lib/sample.js similarity index 61% rename from packages/composer-cucumber-steps/features/basic-sample-network/lib/logic.js rename to packages/composer-cucumber-steps/features/basic-sample-network/lib/sample.js index 1e72a8e59f..18fbf97610 100644 --- a/packages/composer-cucumber-steps/features/basic-sample-network/lib/logic.js +++ b/packages/composer-cucumber-steps/features/basic-sample-network/lib/sample.js @@ -14,19 +14,34 @@ /** * Sample transaction processor function. + * @param {org.acme.sample.SampleTransaction} tx The sample transaction instance. + * @transaction */ -function onSampleTransaction(sampleTransaction) { - var oldValue = sampleTransaction.asset.value; - sampleTransaction.asset.value = sampleTransaction.newValue; +function sampleTransaction(tx) { + + // Save the old value of the asset. + var oldValue = tx.asset.value; + + // Update the asset with the new value. + tx.asset.value = tx.newValue; + + // Get the asset registry for the asset. return getAssetRegistry('org.acme.sample.SampleAsset') .then(function (assetRegistry) { - return assetRegistry.update(sampleTransaction.asset); + + // Update the asset in the asset registry. + return assetRegistry.update(tx.asset); + }) .then(function () { + + // Emit an event for the modified asset. var event = getFactory().newEvent('org.acme.sample', 'SampleEvent'); - event.assetId = sampleTransaction.asset.assetId; + event.asset = tx.asset; event.oldValue = oldValue; - event.newValue = sampleTransaction.newValue; + event.newValue = tx.newValue; emit(event); + }); + } diff --git a/packages/composer-cucumber-steps/features/basic-sample-network/models/org.acme.sample.cto b/packages/composer-cucumber-steps/features/basic-sample-network/models/sample.cto similarity index 95% rename from packages/composer-cucumber-steps/features/basic-sample-network/models/org.acme.sample.cto rename to packages/composer-cucumber-steps/features/basic-sample-network/models/sample.cto index c71560223d..c5231468df 100644 --- a/packages/composer-cucumber-steps/features/basic-sample-network/models/org.acme.sample.cto +++ b/packages/composer-cucumber-steps/features/basic-sample-network/models/sample.cto @@ -23,7 +23,7 @@ transaction SampleTransaction identified by transactionId { event SampleEvent identified by eventId { o String eventId - o String assetId + --> SampleAsset asset o String oldValue o String newValue } diff --git a/packages/composer-cucumber-steps/features/basic-sample-network/package.json b/packages/composer-cucumber-steps/features/basic-sample-network/package.json index c05059a267..5b05d11886 100644 --- a/packages/composer-cucumber-steps/features/basic-sample-network/package.json +++ b/packages/composer-cucumber-steps/features/basic-sample-network/package.json @@ -1 +1 @@ -{"name":"basic-sample-network","version":"0.0.9","description":"The Hello World of Hyperledger Composer samples","scripts":{"prepublish":"mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/basic-sample-network.bna","pretest":"npm run lint","lint":"eslint .","postlint":"npm run licchk","licchk":"license-check","postlicchk":"npm run doc","doc":"jsdoc --pedantic --recurse -c jsdoc.conf","test":"mocha --recursive","deploy":"./scripts/deploy.sh"},"repository":{"type":"git","url":"https://github.com/hyperledger/composer-sample-networks.git"},"keywords":["sample","network"],"author":"Hyperledger Composer","license":"Apache-2.0","devDependencies":{"browserfs":"^1.2.0","chai":"^3.5.0","composer-admin":"latest","composer-cli":"latest","composer-client":"latest","composer-connector-embedded":"latest","eslint":"^3.6.1","istanbul":"^0.4.5","jsdoc":"^3.4.1","license-check":"^1.1.5","mkdirp":"^0.5.1","mocha":"^3.2.0","moment":"^2.17.1"},"license-check-config":{"src":["**/*.js","!./coverage/**/*","!./node_modules/**/*","!./out/**/*","!./scripts/**/*"],"path":"header.txt","blocking":true,"logInfo":false,"logError":true}} \ No newline at end of file +{"name":"basic-sample-network","version":"0.1.0","description":"The Hello World of Hyperledger Composer samples","scripts":{"prepublish":"mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/basic-sample-network.bna","pretest":"npm run lint","lint":"eslint .","postlint":"npm run licchk","licchk":"license-check","postlicchk":"npm run doc","doc":"jsdoc --pedantic --recurse -c jsdoc.conf","test-inner":"mocha --recursive && cucumber-js","test-cover":"nyc npm run test-inner","test":"npm run test-inner"},"repository":{"type":"git","url":"https://github.com/hyperledger/composer-sample-networks.git"},"keywords":["sample","network"],"author":"Hyperledger Composer","license":"Apache-2.0","devDependencies":{"browserfs":"^1.2.0","chai":"^3.5.0","chai-as-promised":"^6.0.0","composer-admin":"latest","composer-cli":"latest","composer-client":"latest","composer-connector-embedded":"latest","composer-cucumber-steps":"latest","cucumber":"^2.2.0","eslint":"^3.6.1","istanbul":"^0.4.5","jsdoc":"^3.4.1","license-check":"^1.1.5","mkdirp":"^0.5.1","mocha":"^3.2.0","moment":"^2.17.1","nyc":"^11.0.2"},"license-check-config":{"src":["**/*.js","!./coverage/**/*","!./node_modules/**/*","!./out/**/*","!./scripts/**/*"],"path":"header.txt","blocking":true,"logInfo":false,"logError":true},"nyc":{"exclude":["coverage/**","features/**","out/**","test/**"],"reporter":["text-summary","html"],"all":true,"check-coverage":true,"statements":100,"branches":100,"functions":100,"lines":100}} \ No newline at end of file diff --git a/packages/composer-cucumber-steps/features/basic-sample-network/permissions.acl b/packages/composer-cucumber-steps/features/basic-sample-network/permissions.acl index 0b87760325..8496969d23 100644 --- a/packages/composer-cucumber-steps/features/basic-sample-network/permissions.acl +++ b/packages/composer-cucumber-steps/features/basic-sample-network/permissions.acl @@ -1,13 +1,14 @@ /** * Sample access control list. */ -rule EverybodyCanRead { +rule EverybodyCanReadEverything { description: "Allow all participants read access to all resources" participant: "org.acme.sample.SampleParticipant" operation: READ - resource: "org.acme.sample" + resource: "org.acme.sample.*" action: ALLOW } + rule EverybodyCanSubmitTransactions { description: "Allow all participants to submit transactions" participant: "org.acme.sample.SampleParticipant" @@ -16,8 +17,8 @@ rule EverybodyCanSubmitTransactions { action: ALLOW } -rule OwnerHasFullAccess { - description: "Allow participants full access to their resources" +rule OwnerHasFullAccessToTheirAssets { + description: "Allow all participants full access to their assets" participant(p): "org.acme.sample.SampleParticipant" operation: ALL resource(r): "org.acme.sample.SampleAsset" diff --git a/packages/composer-cucumber-steps/features/events.feature b/packages/composer-cucumber-steps/features/events.feature index ab42d81fea..5840f13df1 100644 --- a/packages/composer-cucumber-steps/features/events.feature +++ b/packages/composer-cucumber-steps/features/events.feature @@ -14,7 +14,7 @@ Feature: Event steps | asset | newValue | | 1 | 100 | Then I should have received the following event of type org.acme.sample.SampleEvent - | assetId | oldValue | newValue | + | asset | oldValue | newValue | | 1 | 10 | 100 | Scenario: then I should have received the following events @@ -23,7 +23,7 @@ Feature: Event steps | 1 | 100 | Then I should have received the following event """ - {"$class":"org.acme.sample.SampleEvent", "assetId":"1", "oldValue":"10", "newValue":"100"} + {"$class":"org.acme.sample.SampleEvent", "asset":"1", "oldValue":"10", "newValue":"100"} """ Scenario: then I should have received the following events of type (1) @@ -32,7 +32,7 @@ Feature: Event steps | 1 | 20 | | 1 | 100 | Then I should have received the following event of type org.acme.sample.SampleEvent - | assetId | oldValue | newValue | + | asset | oldValue | newValue | | 1 | 10 | 20 | | 1 | 20 | 100 | @@ -44,13 +44,13 @@ Feature: Event steps Then I should have received the following events """ [ - {"$class":"org.acme.sample.SampleEvent", "assetId":"1", "oldValue":"10", "newValue":"20"}, - {"$class":"org.acme.sample.SampleEvent", "assetId":"1", "oldValue":"20", "newValue":"100"} + {"$class":"org.acme.sample.SampleEvent", "asset":"1", "oldValue":"10", "newValue":"20"}, + {"$class":"org.acme.sample.SampleEvent", "asset":"1", "oldValue":"20", "newValue":"100"} ] """ Scenario: I should get an error when I do not receive an event of type (1) Then I should have received the following event of type org.acme.sample.SampleEvent - | assetId | oldValue | newValue | + | asset | oldValue | newValue | | 1 | 10 | 20 | And I should get an error matching /failed to find expected event/ diff --git a/packages/composer-cucumber-steps/package.json b/packages/composer-cucumber-steps/package.json index 73425033ef..1bbf3a5b65 100644 --- a/packages/composer-cucumber-steps/package.json +++ b/packages/composer-cucumber-steps/package.json @@ -1,6 +1,6 @@ { "name": "composer-cucumber-steps", - "version": "0.8.2", + "version": "0.9.0", "description": "A library of Cucumber steps for testing Hyperledger Composer", "main": "index.js", "scripts": { @@ -70,10 +70,10 @@ "dependencies": { "browserfs": "^1.1.0", "chai": "^3.5.0", - "composer-admin": "^0.8.2", - "composer-client": "^0.8.2", - "composer-common": "^0.8.2", - "composer-connector-embedded": "^0.8.2", + "composer-admin": "^0.9.0", + "composer-client": "^0.9.0", + "composer-common": "^0.9.0", + "composer-connector-embedded": "^0.9.0", "thenify-all": "^1.6.0" } } diff --git a/packages/composer-playground-api/package.json b/packages/composer-playground-api/package.json index 042dc4afd0..f6a3e7f1b2 100644 --- a/packages/composer-playground-api/package.json +++ b/packages/composer-playground-api/package.json @@ -1,6 +1,6 @@ { "name": "composer-playground-api", - "version": "0.8.2", + "version": "0.9.0", "description": "The REST API for the Hyperledger Composer Playground", "engines": { "node": ">=6", @@ -57,8 +57,8 @@ }, "dependencies": { "body-parser": "^1.17.0", - "composer-common": "^0.8.2", - "composer-connector-server": "^0.8.2", + "composer-common": "^0.9.0", + "composer-connector-server": "^0.9.0", "dotenv": "^4.0.0", "express": "^4.15.2", "http-status": "^1.0.1", diff --git a/packages/composer-playground/package.json b/packages/composer-playground/package.json index 1a30e55be3..ebd36bca25 100644 --- a/packages/composer-playground/package.json +++ b/packages/composer-playground/package.json @@ -1,6 +1,6 @@ { "name": "composer-playground", - "version": "0.8.2", + "version": "0.9.0", "description": "A test harness/UI for the web runtime container for Hyperledger Composer", "engines": { "node": ">=6", @@ -78,8 +78,8 @@ "dependencies": { "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.24", "cheerio": "^0.22.0", - "composer-common": "^0.8.2", - "composer-playground-api": "^0.8.2", + "composer-common": "^0.9.0", + "composer-playground-api": "^0.9.0", "express": "^4.15.2", "fast-json-patch": "^1.1.8", "file-saver": "^1.3.3", @@ -134,12 +134,12 @@ "chai": "^3.5.0", "codelyzer": "^2.0.1", "codemirror": "5.26.0", - "composer-admin": "^0.8.2", - "composer-client": "^0.8.2", - "composer-connector-proxy": "^0.8.2", - "composer-connector-web": "^0.8.2", - "composer-runtime": "^0.8.2", - "composer-runtime-web": "^0.8.2", + "composer-admin": "^0.9.0", + "composer-client": "^0.9.0", + "composer-connector-proxy": "^0.9.0", + "composer-connector-web": "^0.9.0", + "composer-runtime": "^0.9.0", + "composer-runtime-web": "^0.9.0", "copy-webpack-plugin": "^4.0.1", "core-js": "^2.4.1", "css-loader": "^0.26.1", diff --git a/packages/composer-rest-server/package.json b/packages/composer-rest-server/package.json index 4fab5327cd..676bc18032 100644 --- a/packages/composer-rest-server/package.json +++ b/packages/composer-rest-server/package.json @@ -1,6 +1,6 @@ { "name": "composer-rest-server", - "version": "0.8.2", + "version": "0.9.0", "description": "Hyperledger Composer REST server that uses the Hyperledger Composer LoopBack connector", "engines": { "node": ">=6", @@ -36,7 +36,7 @@ "chalk": "^1.1.3", "clear": "0.0.1", "clui": "^0.3.1", - "composer-common": "^0.8.2", + "composer-common": "^0.9.0", "compression": "^1.0.3", "connect-ensure-login": "^0.1.1", "cookie-parser": "^1.4.3", @@ -52,7 +52,7 @@ "loopback-boot": "^2.23.0", "loopback-component-explorer": "^4.1.1", "loopback-component-passport": "^3.2.0", - "loopback-connector-composer": "^0.8.2", + "loopback-connector-composer": "^0.9.0", "passport-local": "^1.0.0", "serve-favicon": "^2.0.1", "strong-error-handler": "^1.0.1", @@ -64,9 +64,9 @@ "chai-as-promised": "^6.0.0", "chai-http": "^3.0.0", "clone": "^2.1.1", - "composer-admin": "^0.8.2", - "composer-client": "^0.8.2", - "composer-connector-embedded": "^0.8.2", + "composer-admin": "^0.9.0", + "composer-client": "^0.9.0", + "composer-connector-embedded": "^0.9.0", "eslint": "^3.17.1", "jsdoc": "^3.4.3", "license-check": "^1.1.5", diff --git a/packages/composer-rest-server/test/bond-network.bna b/packages/composer-rest-server/test/bond-network.bna index 68edd904ea..2b8c6e6bda 100644 Binary files a/packages/composer-rest-server/test/bond-network.bna and b/packages/composer-rest-server/test/bond-network.bna differ diff --git a/packages/composer-runtime-embedded/package.json b/packages/composer-runtime-embedded/package.json index db8d4817a1..9f772f2301 100644 --- a/packages/composer-runtime-embedded/package.json +++ b/packages/composer-runtime-embedded/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime-embedded", - "version": "0.8.2", + "version": "0.9.0", "description": "The embedded runtime container for Hyperledger Composer", "engines": { "node": ">=6", @@ -52,8 +52,8 @@ "logError": true }, "dependencies": { - "composer-common": "^0.8.2", - "composer-runtime": "^0.8.2", + "composer-common": "^0.9.0", + "composer-runtime": "^0.9.0", "debug": "^2.6.2", "dexie": "^1.5.1", "fake-indexeddb": "^1.0.8", diff --git a/packages/composer-runtime-hlf/package.json b/packages/composer-runtime-hlf/package.json index 94c9560a2e..d840edfd4d 100644 --- a/packages/composer-runtime-hlf/package.json +++ b/packages/composer-runtime-hlf/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime-hlf", - "version": "0.8.2", + "version": "0.9.0", "description": "The Hyperledger Fabric runtime container for Hyperledger Composer", "engines": { "node": ">=6", @@ -28,7 +28,7 @@ "babelify": "^7.3.0", "browserify": "^13.3.0", "browserify-replace": "^0.9.0", - "composer-runtime": "^0.8.2", + "composer-runtime": "^0.9.0", "exorcist": "^0.4.0", "fs-extra": "^1.0.0", "uglify-js": "2.7.5" diff --git a/packages/composer-runtime-hlfv1/package.json b/packages/composer-runtime-hlfv1/package.json index 897a9d8e26..7b3829c63c 100644 --- a/packages/composer-runtime-hlfv1/package.json +++ b/packages/composer-runtime-hlfv1/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime-hlfv1", - "version": "0.8.2", + "version": "0.9.0", "description": "The Hyperledger Fabric v1.x runtime container for Hyperledger Composer", "engines": { "node": ">=6", @@ -28,7 +28,7 @@ "babelify": "^7.3.0", "browserify": "^13.3.0", "browserify-replace": "^0.9.0", - "composer-runtime": "^0.8.2", + "composer-runtime": "^0.9.0", "exorcist": "^0.4.0", "fs-extra": "^1.0.0", "uglify-js": "2.7.5" diff --git a/packages/composer-runtime-web/package.json b/packages/composer-runtime-web/package.json index 6b16215259..f476df0ab6 100644 --- a/packages/composer-runtime-web/package.json +++ b/packages/composer-runtime-web/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime-web", - "version": "0.8.2", + "version": "0.9.0", "description": "The web runtime container for Hyperledger Composer", "engines": { "node": ">=6", @@ -62,8 +62,8 @@ "logError": true }, "dependencies": { - "composer-common": "^0.8.2", - "composer-runtime": "^0.8.2", + "composer-common": "^0.9.0", + "composer-runtime": "^0.9.0", "dexie": "^1.5.1", "uuid": "^3.0.1", "xhr": "^2.4.0" diff --git a/packages/composer-runtime/lib/accesscontroller.js b/packages/composer-runtime/lib/accesscontroller.js index 3c5f408133..5f8762dd4c 100644 --- a/packages/composer-runtime/lib/accesscontroller.js +++ b/packages/composer-runtime/lib/accesscontroller.js @@ -16,6 +16,7 @@ const AccessException = require('./accessexception'); const Logger = require('composer-common').Logger; +const ModelUtil = require('composer-common').ModelUtil; const LOG = Logger.getLog('AccessController'); @@ -208,22 +209,16 @@ class AccessController { const method = 'matchNoun'; LOG.entry(method, resource.getFullyQualifiedIdentifier(), aclRule); - // Determine the input fully qualified name and ID. - let fqn = resource.getFullyQualifiedType(); - let ns = resource.getNamespace(); + // Determine the input ID. let id = resource.getIdentifier(); - // Check the namespace and type of the ACL rule. + // Check to see if the resource is an instance of the + // required resource type, or is in the required + // namespace. let noun = aclRule.getNoun(); - - // Check to see if the fully qualified name matches. let reqFQN = noun.getFullyQualifiedName(); - if (fqn === reqFQN) { - // Noun is matching fully qualified type. - } else if (ns === reqFQN) { - // Noun is matching namespace. - } else { - // Noun does not match. + + if (!ModelUtil.isMatchingType(resource, reqFQN)) { LOG.exit(method, false); return false; } @@ -292,14 +287,9 @@ class AccessController { // Check to see if the participant is an instance of the // required participant type, or is in the required // namespace. - let ns = participant.getNamespace(); let reqFQN = reqParticipant.getFullyQualifiedName(); - if (participant.instanceOf(reqFQN)) { - // Participant is matching type or subtype. - } else if (ns === reqFQN) { - // Participant is matching namespace. - } else { - // Participant does not match. + + if (!ModelUtil.isMatchingType(participant, reqFQN)) { LOG.exit(method, false); return false; } @@ -352,14 +342,9 @@ class AccessController { // Check to see if the participant is an instance of the // required participant type, or is in the required // namespace. - let ns = transaction.getNamespace(); let reqFQN = reqTransaction.getFullyQualifiedName(); - if (transaction.instanceOf(reqFQN)) { - // Transaction is matching type or subtype. - } else if (ns === reqFQN) { - // Transaction is matching namespace. - } else { - // Participant does not match. + + if (!ModelUtil.isMatchingType(transaction, reqFQN)) { LOG.exit(method, false); return false; } diff --git a/packages/composer-runtime/package.json b/packages/composer-runtime/package.json index 7d3dd34745..5d821e513b 100644 --- a/packages/composer-runtime/package.json +++ b/packages/composer-runtime/package.json @@ -1,6 +1,6 @@ { "name": "composer-runtime", - "version": "0.8.2", + "version": "0.9.0", "description": "The runtime execution environment for Hyperledger Composer", "engines": { "node": ">=6", @@ -62,7 +62,7 @@ "logError": true }, "dependencies": { - "composer-common": "^0.8.2", + "composer-common": "^0.9.0", "debug": "^2.6.2", "fast-json-patch": "^1.1.8", "jsonata": "^1.2.2", diff --git a/packages/composer-runtime/test/accesscontroller.js b/packages/composer-runtime/test/accesscontroller.js index e39c948c04..04dba48651 100644 --- a/packages/composer-runtime/test/accesscontroller.js +++ b/packages/composer-runtime/test/accesscontroller.js @@ -30,6 +30,7 @@ describe('AccessController', () => { let aclManager; let factory; let asset; + let asset2; let participant; let participant2; let transaction; @@ -57,7 +58,10 @@ describe('AccessController', () => { asset TestAsset identified by assetId extends BaseAsset { o String assetId } - asset TestAsset2 identified by assetId extends BaseAsset { + asset TestAsset2 extends TestAsset { + + } + asset TestAsset3 identified by assetId extends BaseAsset { o String assetId } participant TestParticipant identified by participantId extends BaseParticipant { @@ -94,6 +98,7 @@ describe('AccessController', () => { aclManager = new AclManager(modelManager); factory = new Factory(modelManager); asset = factory.newResource('org.acme.test', 'TestAsset', 'A1234'); + asset2 = factory.newResource('org.acme.test', 'TestAsset2', 'A4321'); participant = factory.newResource('org.acme.test', 'TestParticipant', 'P5678'); participant2 = factory.newResource('org.acme.test', 'TestParticipant2', 'P7890'); transaction = factory.newResource('org.acme.test', 'TestTransaction', 'T9012'); @@ -325,7 +330,13 @@ describe('AccessController', () => { }); it('should return true if the ACL rule specifies a matching namespace', () => { - setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test.TestParticipant#P5678" operation: READ resource: "org.acme.test" action: ALLOW}'); + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test.TestParticipant#P5678" operation: READ resource: "org.acme.test.*" action: ALLOW}'); + controller.matchNoun(asset, aclManager.getAclRules()[0]) + .should.be.true; + }); + + it('should return true if the ACL rule specifies a matching recursive namespace', () => { + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test.TestParticipant#P5678" operation: READ resource: "org.acme.**" action: ALLOW}'); controller.matchNoun(asset, aclManager.getAclRules()[0]) .should.be.true; }); @@ -343,7 +354,27 @@ describe('AccessController', () => { }); it('should return false if the ACL rule specifies a non-matching namespace', () => { - setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test.TestParticipant#P5678" operation: READ resource: "org.acme.test2" action: ALLOW}'); + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test.TestParticipant#P5678" operation: READ resource: "org.acme.test2.*" action: ALLOW}'); + controller.matchNoun(asset, aclManager.getAclRules()[0]) + .should.be.false; + }); + + it('should return false if the ACL rule specifies a non-matching recursive namespace', () => { + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test.TestParticipant#P5678" operation: READ resource: "org.acme.test2.**" action: ALLOW}'); + controller.matchNoun(asset, aclManager.getAclRules()[0]) + .should.be.false; + }); + + it('should return true if the ACL rule specifies a fully qualified name of a nested supertype', () => { + // Test with TestAsset2 which extends TestAsset which extends BaseAsset. + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test.TestParticipant#P5678" operation: READ resource: "org.acme.base.BaseAsset" action: ALLOW}'); + controller.matchNoun(asset2, aclManager.getAclRules()[0]) + .should.be.true; + }); + + it('should return false if the ACL rule specifies a fully qualified name of a subtype', () => { + // Test with TestAsset which is extended by TestAsset3. + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test.TestParticipant#P5678" operation: READ resource: "org.acme.test.TestAsset3" action: ALLOW}'); controller.matchNoun(asset, aclManager.getAclRules()[0]) .should.be.false; }); @@ -404,7 +435,13 @@ describe('AccessController', () => { }); it('should return true if the ACL rule specifies a matching namespace', () => { - setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test" operation: READ resource: "org.acme.test.TestAsset#A1234" action: ALLOW}'); + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test.*" operation: READ resource: "org.acme.test.TestAsset#A1234" action: ALLOW}'); + controller.matchParticipant(participant, aclManager.getAclRules()[0]) + .should.be.true; + }); + + it('should return true if the ACL rule specifies a matching recursive namespace', () => { + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.**" operation: READ resource: "org.acme.test.TestAsset#A1234" action: ALLOW}'); controller.matchParticipant(participant, aclManager.getAclRules()[0]) .should.be.true; }); @@ -422,7 +459,13 @@ describe('AccessController', () => { }); it('should return false if the ACL rule specifies a non-matching namespace', () => { - setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test2" operation: READ resource: "org.acme.test.TestAsset#A1234" action: ALLOW}'); + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test2.*" operation: READ resource: "org.acme.test.TestAsset#A1234" action: ALLOW}'); + controller.matchParticipant(participant, aclManager.getAclRules()[0]) + .should.be.false; + }); + + it('should return false if the ACL rule specifies a non-matching recursive namespace', () => { + setAclFile('rule R1 {description: "Test R1" participant: "org.acme.test2.**" operation: READ resource: "org.acme.test.TestAsset#A1234" action: ALLOW}'); controller.matchParticipant(participant, aclManager.getAclRules()[0]) .should.be.false; }); @@ -472,7 +515,13 @@ describe('AccessController', () => { }); it('should return true if the ACL rule specifies a matching namespace', () => { - setAclFile('rule R1 {description: "Test R1" participant: "ANY" operation: READ resource: "org.acme.test.TestAsset#A1234" transaction: "org.acme.test" action: ALLOW}'); + setAclFile('rule R1 {description: "Test R1" participant: "ANY" operation: READ resource: "org.acme.test.TestAsset#A1234" transaction: "org.acme.test.*" action: ALLOW}'); + controller.matchTransaction(transaction, aclManager.getAclRules()[0]) + .should.be.true; + }); + + it('should return true if the ACL rule specifies a matching recusive namespace', () => { + setAclFile('rule R1 {description: "Test R1" participant: "ANY" operation: READ resource: "org.acme.test.TestAsset#A1234" transaction: "org.acme.test.**" action: ALLOW}'); controller.matchTransaction(transaction, aclManager.getAclRules()[0]) .should.be.true; }); @@ -484,7 +533,13 @@ describe('AccessController', () => { }); it('should return false if the ACL rule specifies a non-matching namespace', () => { - setAclFile('rule R1 {description: "Test R1" participant: "ANY" operation: READ resource: "org.acme.test.TestAsset#A1234" transaction: "org.acme.test2" action: ALLOW}'); + setAclFile('rule R1 {description: "Test R1" participant: "ANY" operation: READ resource: "org.acme.test.TestAsset#A1234" transaction: "org.acme.test2.*" action: ALLOW}'); + controller.matchTransaction(transaction, aclManager.getAclRules()[0]) + .should.be.false; + }); + + it('should return false if the ACL rule specifies a non-matching recursive namespace', () => { + setAclFile('rule R1 {description: "Test R1" participant: "ANY" operation: READ resource: "org.acme.test.TestAsset#A1234" transaction: "org.acme.test2.**" action: ALLOW}'); controller.matchTransaction(transaction, aclManager.getAclRules()[0]) .should.be.false; }); diff --git a/packages/composer-systests/package.json b/packages/composer-systests/package.json index 1718a0d0da..261fc9a2c7 100644 --- a/packages/composer-systests/package.json +++ b/packages/composer-systests/package.json @@ -1,6 +1,6 @@ { "name": "composer-systests", - "version": "0.8.2", + "version": "0.9.0", "private": true, "description": "System tests and automation for Hyperledger Composer", "engines": { @@ -42,13 +42,13 @@ "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "chai-subset": "^1.3.0", - "composer-admin": "^0.8.2", - "composer-client": "^0.8.2", - "composer-common": "^0.8.2", - "composer-connector-embedded": "^0.8.2", - "composer-connector-proxy": "^0.8.2", - "composer-connector-server": "^0.8.2", - "composer-connector-web": "^0.8.2", + "composer-admin": "^0.9.0", + "composer-client": "^0.9.0", + "composer-common": "^0.9.0", + "composer-connector-embedded": "^0.9.0", + "composer-connector-proxy": "^0.9.0", + "composer-connector-server": "^0.9.0", + "composer-connector-web": "^0.9.0", "eslint": "^3.17.1", "homedir": "^0.6.0", "karma": "^1.3.0", diff --git a/packages/composer-website/jekylldocs/assets/img/tutorials/developer/Tutorial2.md b/packages/composer-website/jekylldocs/assets/img/tutorials/developer/Tutorial2.md index b51fc2c57f..c7e1540a21 100644 --- a/packages/composer-website/jekylldocs/assets/img/tutorials/developer/Tutorial2.md +++ b/packages/composer-website/jekylldocs/assets/img/tutorials/developer/Tutorial2.md @@ -138,7 +138,7 @@ rule Default { description: "Allow all participants access to all resources" participant: "ANY" operation: ALL -resource: "org.example.mynetwork" +resource: "org.example.mynetwork.*" action: ALLOW } ``` diff --git a/packages/composer-website/jekylldocs/reference/acl_language.md b/packages/composer-website/jekylldocs/reference/acl_language.md index 2c358547d7..d844c880f2 100644 --- a/packages/composer-website/jekylldocs/reference/acl_language.md +++ b/packages/composer-website/jekylldocs/reference/acl_language.md @@ -51,12 +51,12 @@ rule SampleConditionalRule { Multiple ACL rules may be defined that conceptually define a decision table. The actions of the decision tree define access control decisions (ALLOW or DENY). If the decision table fails to match then by default access is denied. -**Resource** defines the things that the ACL rule applies to. This can be a property on a class, an entire class or all classes within a namespace. It can also be an instance of a class. +**Resource** defines the things that the ACL rule applies to. This can be a class, all classes within a namespace, or all classes under a namespace. It can also be an instance of a class. Resource Examples: -- Namespace: org.example +- Namespace: org.example.* +- Namespace (recursive): org.example.** - Class in namespace: org.example.Car -- Property on class: org.example.Car.owner - Instance of a class: org.example.Car#ABC123 **Operation** identifies the action that the rule governs. It must be one of: CREATE, READ, UPDATE, DELETE or ALL. @@ -90,15 +90,6 @@ rule R2 { } rule R3 { - description: "Driver can change the ownership of a car that they own" - participant(d): "org.example.Driver" - operation: UPDATE - resource(o): "org.example.Car" - condition: (o == d) - action: ALLOW -} - -rule R4 { description: "regulators can perform all operations on Cars" participant: "org.example.Regulator" operation: ALL @@ -106,11 +97,19 @@ rule R4 { action: ALLOW } -rule R5 { +rule R4 { description: "Everyone can read all resources in the org.example namespace" participant: "ANY" operation: READ - resource: "org.example" + resource: "org.example.*" + action: ALLOW +} + +rule R5 { + description: "Everyone can read all resources under the org.example namespace" + participant: "ANY" + operation: READ + resource: "org.example.**" action: ALLOW } ``` diff --git a/packages/composer-website/jekylldocs/tutorials/developer-guide.md b/packages/composer-website/jekylldocs/tutorials/developer-guide.md index d20e22d670..c06d9ede1f 100644 --- a/packages/composer-website/jekylldocs/tutorials/developer-guide.md +++ b/packages/composer-website/jekylldocs/tutorials/developer-guide.md @@ -179,7 +179,7 @@ rule Default { description: "Allow all participants access to all resources" participant: "ANY" operation: ALL - resource: "org.acme.mynetwork" + resource: "org.acme.mynetwork.*" action: ALLOW } ``` diff --git a/packages/composer-website/package.json b/packages/composer-website/package.json index f2e20b7000..6711850d5b 100644 --- a/packages/composer-website/package.json +++ b/packages/composer-website/package.json @@ -1,6 +1,6 @@ { "name": "composer-website", - "version": "0.8.2", + "version": "0.9.0", "private": true, "description": "Hyperledger Composer is a blockchain development framework for Hyperledger Fabric: a library of assets/functions for creating blockchain-based applications.", "engines": { @@ -28,10 +28,10 @@ "author": "Hyperledger Composer", "license": "Apache-2.0", "devDependencies": { - "composer-admin": "^0.8.2", - "composer-client": "^0.8.2", - "composer-common": "^0.8.2", - "composer-runtime": "^0.8.2", + "composer-admin": "^0.9.0", + "composer-client": "^0.9.0", + "composer-common": "^0.9.0", + "composer-runtime": "^0.9.0", "jsdoc": "^3.4.3", "node-plantuml": "^0.5.0", "sanitize-html": "^1.14.1" diff --git a/packages/generator-hyperledger-composer/package.json b/packages/generator-hyperledger-composer/package.json index 3d08ae74ef..c34d45e907 100755 --- a/packages/generator-hyperledger-composer/package.json +++ b/packages/generator-hyperledger-composer/package.json @@ -1,7 +1,7 @@ { "name": "generator-hyperledger-composer", - "version": "0.8.2", - "description": "Generate projects from Hyperledger Composer business network definitions", + "version": "0.9.0", + "description": "Generates projects from Hyperledger Composer business network definitions", "engines": { "node": ">=6", "npm": ">=3" @@ -15,8 +15,8 @@ "liveNetworkTest": "mocha -t 0 test/angular-network.js" }, "dependencies": { - "composer-client": "^0.8.2", - "composer-common": "^0.8.2", + "composer-client": "^0.9.0", + "composer-common": "^0.9.0", "fs": "0.0.1-security", "shelljs": "^0.7.7", "underscore.string": "^3.3.4", @@ -30,8 +30,8 @@ "license": "Apache-2.0", "devDependencies": { "@angular/cli": "^1.0.0-rc.0", - "composer-admin": "^0.8.2", - "composer-connector-embedded": "^0.8.2", + "composer-admin": "^0.9.0", + "composer-connector-embedded": "^0.9.0", "typings": "^2.1.0", "yeoman-test": "^1.6.0" } diff --git a/packages/loopback-connector-composer/package.json b/packages/loopback-connector-composer/package.json index e418f11bc3..11c990a892 100644 --- a/packages/loopback-connector-composer/package.json +++ b/packages/loopback-connector-composer/package.json @@ -1,6 +1,6 @@ { "name": "loopback-connector-composer", - "version": "0.8.2", + "version": "0.9.0", "description": "A Loopback connector for Hyperledger Composer", "engines": { "node": ">=6", @@ -26,8 +26,8 @@ "author": "Hyperledger Composer", "license": "Apache-2.0", "dependencies": { - "composer-client": "^0.8.2", - "composer-common": "^0.8.2", + "composer-client": "^0.9.0", + "composer-common": "^0.9.0", "loopback": "^3.4.0", "loopback-connector": "^4.0.0", "node-cache": "^4.1.1" @@ -35,8 +35,8 @@ "devDependencies": { "chai": "^3.5.0", "chai-as-promised": "^6.0.0", - "composer-admin": "^0.8.2", - "composer-connector-embedded": "^0.8.2", + "composer-admin": "^0.9.0", + "composer-connector-embedded": "^0.9.0", "eslint": "^3.17.1", "jsdoc": "^3.4.3", "license-check": "^1.1.5", diff --git a/packages/loopback-connector-composer/test/bond-network.bna b/packages/loopback-connector-composer/test/bond-network.bna index 68edd904ea..2b8c6e6bda 100644 Binary files a/packages/loopback-connector-composer/test/bond-network.bna and b/packages/loopback-connector-composer/test/bond-network.bna differ