Source: composer-runtime/lib/accesscontroller.js

/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

const AccessException = require('./accessexception');
const Logger = require('composer-common').Logger;

const LOG = Logger.getLog('AccessController');

/**
 * A class that manages access to registries and resources by processing
 * the access control list(s) in a business network definition.
 * @private
 * @class
 * @memberof module:composer-runtime
 */
class AccessController {

    /**
     * Constructor.
     * @param {AclManager} aclManager The ACL manager to use.
     */
    constructor(aclManager) {
        const method = 'constructor';
        LOG.entry(method, aclManager);
        this.aclManager = aclManager;
        this.participant = null;
        LOG.exit(method);
    }

    /**
     * Get the current participant.
     * @return {Resource} The current participant.
     */
    getParticipant() {
        return this.participant;
    }

    /**
     * Set the current participant.
     * @param {Resource} participant The current participant.
     */
    setParticipant(participant) {
        this.participant = participant;
    }

    /**
     * Check that the specified participant has the specified
     * level of access to the specified resource.
     * @param {Resource} resource The resource.
     * @param {string} access The level of access.
     * @param {Resource} participant The participant.
     * @throws {AccessException} If the specified participant
     * does not have the specified level of access to the specified
     * resource.
     */
    check(resource, access) {
        const method = 'check';
        LOG.entry(method, resource.getFullyQualifiedIdentifier(), access);
        try {

            // Check to see if a participant has been set. If not, then ACL
            // enforcement is not enabled.
            let participant = this.participant;
            if (!participant) {
                LOG.debug(method, 'No participant');
                LOG.exit(method);
                return;
            }

            // Check to see if an ACL file was supplied. If not, then ACL
            // enforcement is not enabled.
            if (!this.aclManager.getAclFile()) {
                LOG.debug(method, 'No ACL file');
                LOG.exit(method);
                return;
            }

            // Iterate over the ACL rules in order, but stop at the first rule
            // that permits the action.
            let aclRules = this.aclManager.getAclRules();
            let result = aclRules.some((aclRule) => {
                LOG.debug(method, 'Processing rule', aclRule);
                let value = this.checkRule(resource, access, participant, aclRule);
                LOG.debug(method, 'Processed rule', value);
                return value;
            });

            // If a ACL rule permitted the action, return.
            if (result) {
                LOG.exit(method);
                return;
            }

            // Otherwise no ACL rule permitted the action.
            throw new AccessException(resource, access, participant);

        } catch (e) {
            LOG.error(method, e);
            throw e;
        }
    }

    /**
     * Check the specified ACL rule permits the specified level
     * of access to the specified resource.
     * @param {Resource} resource The resource.
     * @param {string} access The level of access.
     * @param {Resource} participant The participant.
     * @param {AclRule} aclRule The ACL rule.
     * @returns {boolean} True if the specified ACL rule permits
     * the specified level of access to the specified resource.
     */
    checkRule(resource, access, participant, aclRule) {
        const method = 'checkRule';
        LOG.entry(method, participant.getFullyQualifiedIdentifier(), resource, access, participant, aclRule);

        // Is the ACL rule relevant to the specified noun?
        if (!this.matchNoun(resource, aclRule)) {
            LOG.debug(method, 'Noun does not match');
            LOG.exit(method, false);
            return false;
        }

        // Is the ACL rule relevant to the specified verb?
        if (!this.matchVerb(access, aclRule)) {
            LOG.debug(method, 'Verb does not match');
            LOG.exit(method, false);
            return false;
        }

        // Is the ACL rule relevant to the specified participant?
        if (!this.matchParticipant(participant, aclRule)) {
            LOG.debug(method, 'Participant does not match');
            LOG.exit(method, false);
            return false;
        }

        // Is the predicate met?
        if (!this.matchPredicate(resource, access, participant, aclRule)) {
            LOG.debug(method, 'Predicate does not match');
            LOG.exit(method, false);
            return false;
        }

        // Yes, is this an allow or deny rule?
        if (aclRule.getAction() === 'ALLOW') {
            LOG.exit(method, true);
            return true;
        }

        // This must be an explicit deny rule, so throw.
        let e = new AccessException(resource, access, participant);
        LOG.error(method, e);
        throw e;

    }

    /**
     * Check that the specified participant has the specified
     * level of access to the specified resource.
     * @param {Resource} resource The resource.
     * @param {AclRule} aclRule The ACL rule.
     * @returns {boolean} True if the specified ACL rule permits
     * the specified level of access to the specified resource.
     */
    matchNoun(resource, aclRule) {
        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();
        let id = resource.getIdentifier();

        // Check the namespace and type of the ACL rule.
        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.
            LOG.exit(method, false);
            return false;
        }

        // Check to see if the identifier matches (if specified).
        let reqID = noun.getInstanceIdentifier();
        if (reqID) {
            if (id === reqID) {
                // Noun is matching identifier.
            } else {
                // Noun does not match.
                LOG.exit(method, false);
                return false;
            }
        } else {
            // Noun does not specify identifier.
        }

        LOG.exit(method, true);
        return true;
    }

    /**
     * Check that the specified participant has the specified
     * level of access to the specified resource.
     * @param {string} access The level of access.
     * @param {AclRule} aclRule The ACL rule.
     * @returns {boolean} True if the specified ACL rule permits
     * the specified level of access to the specified resource.
     */
    matchVerb(access, aclRule) {
        const method = 'matchVerb';
        LOG.entry(method, access, aclRule);

        // Check to see if the access matches the verb of the ACL rule.
        // Verb can be one of:
        //   'CREATE' / 'READ' / 'UPDATE' / 'ALL' / 'DELETE'
        let result = false;
        let verb = aclRule.getVerb();
        if (verb === 'ALL' || access === verb) {
            result = true;
        }

        LOG.exit(method, result);
        return result;
    }

    /**
     * Check that the specified participant has the specified
     * level of access to the specified resource.
     * @param {Resource} participant The participant.
     * @param {AclRule} aclRule The ACL rule.
     * @returns {boolean} True if the specified ACL rule permits
     * the specified level of access to the specified resource.
     */
    matchParticipant(participant, aclRule) {
        const method = 'matchParticipant';
        LOG.entry(method, participant.getFullyQualifiedIdentifier(), aclRule);

        // Is a participant specified in the ACL rule?
        let reqParticipant = aclRule.getParticipant();
        if (!reqParticipant) {
            LOG.exit(method, true);
            return true;
        }

        // Determine the input fully qualified name and ID.
        let ns = participant.getNamespace();
        let fqn = participant.getFullyQualifiedType();
        let id = participant.getIdentifier();

        // Check to see if the fully qualified name matches.
        let reqFQN = reqParticipant.getFullyQualifiedName();
        if (fqn === reqFQN) {
            // Participant is matching fully qualified type.
        } else if (ns === reqFQN) {
            // Participant is matching namespace.
        } else {
            // Participant does not match.
            LOG.exit(method, false);
            return false;
        }

        // Check to see if the identifier matches (if specified).
        let reqID = reqParticipant.getInstanceIdentifier();
        if (reqID) {
            if (id === reqID) {
                // Participant is matching identifier.
            } else {
                // Participant does not match.
                LOG.exit(method, false);
                return false;
            }
        } else {
            // Participant does not specify identifier.
        }

        LOG.exit(method, true);
        return true;
    }

    /**
     * Check that the specified participant has the specified
     * level of access to the specified resource.
     * @param {Resource} resource The resource.
     * @param {string} access The level of access.
     * @param {Resource} participant The participant.
     * @param {AclRule} aclRule The ACL rule.
     * @returns {boolean} True if the specified ACL rule permits
     * the specified level of access to the specified resource.
     */
    matchPredicate(resource, access, participant, aclRule) {
        const method = 'matchPredicate';
        LOG.entry(method, resource.getFullyQualifiedIdentifier(), access, participant.getFullyQualifiedIdentifier(), aclRule);

        // Get the predicate from the rule.
        let predicate = aclRule.getPredicate().getExpression();

        // We can fast track the simple boolean predicates.
        if (predicate === 'true') {
            LOG.exit(method, true);
            return true;
        } else if (predicate === 'false') {
            LOG.exit(method, false);
            return false;
        }

        // Otherwise we need to build a function.
        let source = `return (${predicate});`;
        let argNames = [];
        let argValues = [];

        // Check to see if the resource needs to be bound.
        let resourceVar = aclRule.getNoun().getVariableName();
        if (resourceVar) {
            argNames.push(resourceVar);
            argValues.push(resource);
        }

        // Check to see if the participant needs to be bound.
        let reqParticipant = aclRule.getParticipant();
        if (reqParticipant) {
            let participantVar = aclRule.getParticipant().getVariableName();
            if (participantVar) {
                argNames.push(participantVar);
                argValues.push(participant);
            }
        }

        // Compile and execute the function.
        let result;
        try {
            LOG.debug(method, 'Compiling and executing function', source, argNames, argValues);
            let func = new Function(argNames.join(','), source);
            result = func.apply(null, argValues);
        } catch (e) {
            LOG.error(method, e);
            throw new AccessException(resource, access, participant);
        }

        // Convert the result into a boolean before returning it.
        result = !!result;
        LOG.exit(method, result);
        return result;
    }

}

module.exports = AccessController;