ddf.security.pdp.realm.xacml.processor.BalanaClient.java Source code

Java tutorial

Introduction

Here is the source code for ddf.security.pdp.realm.xacml.processor.BalanaClient.java

Source

/**
 * Copyright (c) Codice Foundation
 * <p>
 * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation, either version 3 of the
 * License, or any later version.
 * <p>
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details. A copy of the GNU Lesser General Public License
 * is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package ddf.security.pdp.realm.xacml.processor;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.Marshaller;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.sax.SAXSource;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.codice.ddf.parser.Parser;
import org.codice.ddf.parser.ParserConfigurator;
import org.codice.ddf.parser.ParserException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.balana.PDP;
import org.wso2.balana.PDPConfig;
import org.wso2.balana.finder.AttributeFinder;
import org.wso2.balana.finder.AttributeFinderModule;
import org.wso2.balana.finder.PolicyFinder;
import org.wso2.balana.finder.PolicyFinderModule;
import org.wso2.balana.finder.impl.CurrentEnvModule;
import org.wso2.balana.finder.impl.SelectorModule;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLFilterImpl;
import org.xml.sax.helpers.XMLReaderFactory;

import com.google.common.collect.ImmutableList;

import oasis.names.tc.xacml._3_0.core.schema.wd_17.ObjectFactory;
import oasis.names.tc.xacml._3_0.core.schema.wd_17.RequestType;
import oasis.names.tc.xacml._3_0.core.schema.wd_17.ResponseType;

/**
 * Balana implementation of a XACML Policy Decision Point (PDP). This class acts as a proxy to the
 * real Balana PDP.
 */
public class BalanaClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(BalanaClient.class);

    private static final String XACML30_NAMESPACE = "urn:oasis:names:tc:xacml:3.0:core:schema:wd-17";

    private static final String XACML_PREFIX = "xacml";

    private static JAXBContext jaxbContext;

    private static final long DEFAULT_POLLING_INTERVAL_IN_SECONDS = 60;

    private static final String NULL_DIRECTORY_EXCEPTION_MSG = "Cannot read from null XACML Policy Directory";

    static long defaultPollingIntervalInSeconds = 60;

    private PDP pdp;

    private Set<String> xacmlPolicyDirectories;

    private final Parser parser;

    /**
     * Creates the proxy to the real Balana PDP.
     *
     * @param relativeXacmlPoliciesDirectoryPath Relative directory path to the root of the DDF installation.
     * @param parser                             for marshal and unmarshal
     * @throws PdpException
     */
    public BalanaClient(String relativeXacmlPoliciesDirectoryPath, Parser parser) throws PdpException {
        this.parser = parser;
        if (StringUtils.isEmpty(relativeXacmlPoliciesDirectoryPath)) {
            throw new PdpException(NULL_DIRECTORY_EXCEPTION_MSG);
        }

        File xacmlPoliciesDirectory;

        try {
            xacmlPoliciesDirectory = new File(relativeXacmlPoliciesDirectoryPath).getCanonicalFile();
        } catch (IOException e) {
            throw new PdpException(e.getMessage(), e);
        }

        initialize(xacmlPoliciesDirectory);
    }

    private void initialize(File xacmlPoliciesDirectory) throws PdpException {
        try {
            // Only a single default directory is supported
            // If the directory path becomes customizable this
            // functionality should be re-evaluated
            FileUtils.forceMkdir(xacmlPoliciesDirectory);
        } catch (IOException e) {
            LOGGER.error("Unable to create directory: {}", xacmlPoliciesDirectory.getAbsolutePath());
        }
        checkXacmlPoliciesDirectory(xacmlPoliciesDirectory);

        /**
         * We currently only support one XACML policies directory, but we may support multiple
         * directories in the future.
         */
        xacmlPolicyDirectories = new HashSet<>(1);
        xacmlPolicyDirectories.add(xacmlPoliciesDirectory.getPath());
        createPdp(createPdpConfig());
    }

    /**
     * Evaluates the XACML request and returns a XACML response.
     *
     * @param xacmlRequestType XACML request
     * @return XACML response
     * @throws PdpException
     */
    public ResponseType evaluate(RequestType xacmlRequestType) throws PdpException {
        String xacmlRequest = this.marshal(xacmlRequestType);

        String xacmlResponse = this.callPdp(xacmlRequest);

        LOGGER.debug("\nXACML 3.0 Response from Balana PDP:\n {}", xacmlResponse);

        DOMResult domResult = addNamespaceAndPrefixes(xacmlResponse);

        return unmarshal(domResult);
    }

    /**
     * Creates the Balana PDP.
     */
    private void createPdp(PDPConfig pdpConfig) {
        LOGGER.debug("Creating PDP of type: {}", PDP.class.getName());
        pdp = new PDP(pdpConfig);
        LOGGER.debug("PDP creation successful.");
    }

    /**
     * Creates the Balana PDP configuration.
     *
     * @return PDPConfig
     */
    private PDPConfig createPdpConfig() {
        LOGGER.debug("Creating PDP Config.");
        AttributeFinder attributeFinder = new AttributeFinder();
        List<AttributeFinderModule> attributeFinderModules = new ArrayList<AttributeFinderModule>();
        SelectorModule selectorModule = new SelectorModule();
        CurrentEnvModule currentEnvModule = new CurrentEnvModule();
        attributeFinderModules.add(selectorModule);
        attributeFinderModules.add(currentEnvModule);
        attributeFinder.setModules(attributeFinderModules);
        return new PDPConfig(attributeFinder, createPolicyFinder(), null, false);
    }

    /**
     * Creates a policy finder to find XACML polices.
     *
     * @return PolicyFinder
     */
    private PolicyFinder createPolicyFinder() {
        LOGGER.debug("XACML policies will be looked for in the following location(s): {}", xacmlPolicyDirectories);
        PolicyFinder policyFinder = new PolicyFinder();
        PollingPolicyFinderModule policyFinderModule = new PollingPolicyFinderModule(xacmlPolicyDirectories,
                defaultPollingIntervalInSeconds);
        policyFinderModule.start();
        Set<PolicyFinderModule> policyFinderModules = new HashSet<>(1);
        policyFinderModules.add(policyFinderModule);
        policyFinder.setModules(policyFinderModules);

        return policyFinder;
    }

    /**
     * Performs basic checks on the XACML policy directory.
     *
     * @param xacmlPoliciesDirectory The directory containing the XACML policy.
     * @throws PdpException
     */
    private void checkXacmlPoliciesDirectory(File xacmlPoliciesDirectory) throws PdpException {
        StringBuilder message = new StringBuilder();
        boolean errors = false;

        if (!xacmlPoliciesDirectory.isDirectory()) {
            message.append("The XACML policies directory ").append(xacmlPoliciesDirectory.getPath())
                    .append(" does not exist or is not a directory.  ");
            errors = true;
        }

        if (!xacmlPoliciesDirectory.canRead()) {
            message.append("The XACML policies directory ").append(xacmlPoliciesDirectory.getPath())
                    .append(" is not readable.  ");
            errors = true;
        }

        if (errors) {
            throw new PdpException(message.toString());
        }
    }

    /**
     * Calls the real Balana PDP to evaluate the XACML request.
     *
     * @param xacmlRequest The XACML request as a string.
     * @return The XACML response as a string.
     */
    private String callPdp(String xacmlRequest) {

        return pdp.evaluate(xacmlRequest);
    }

    /**
     * Adds namespaces and namespace prefixes to the XACML response returned by the Balana PDP. The
     * Balana PDP returns a response with no namespaces, so we need to add them to unmarshal the
     * response.
     *
     * @param xacmlResponse The XACML response as a string.
     * @return DOM representation of the XACML response with namespaces and namespace prefixes.
     * @throws PdpException
     */
    private DOMResult addNamespaceAndPrefixes(String xacmlResponse) throws PdpException {
        XMLReader xmlReader = null;

        try {
            xmlReader = new XMLFilterImpl(XMLReaderFactory.createXMLReader()) {
                @Override
                public void startElement(String uri, String localName, String qName, Attributes attributes)
                        throws SAXException {
                    super.startElement(XACML30_NAMESPACE, localName, XACML_PREFIX + ":" + qName, attributes);
                }

                @Override
                public void endElement(String uri, String localName, String qName) throws SAXException {
                    super.endElement(XACML30_NAMESPACE, localName, XACML_PREFIX + ":" + qName);
                }
            };
        } catch (SAXException e) {
            String message = "Unable to read XACML response:\n" + xacmlResponse;
            LOGGER.error(message);
            throw new PdpException(message, e);
        }

        DOMResult domResult;
        ClassLoader tccl = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(BalanaClient.class.getClassLoader());
        try {
            TransformerFactory transformerFactory = TransformerFactory.newInstance();

            domResult = new DOMResult();

            Transformer transformer = transformerFactory.newTransformer();
            transformer.transform(new SAXSource(xmlReader, new InputSource(new StringReader(xacmlResponse))),
                    domResult);
        } catch (TransformerException e) {
            String message = "Unable to transform XACML response:\n" + xacmlResponse;
            LOGGER.error(message);
            throw new PdpException(message, e);
        } finally {
            Thread.currentThread().setContextClassLoader(tccl);
        }

        return domResult;
    }

    /**
     * Marshalls the XACML request to a string.
     *
     * @param xacmlRequestType The XACML request to marshal.
     * @return A string representation of the XACML request.
     */
    private String marshal(RequestType xacmlRequestType) throws PdpException {
        if (null == parser) {
            throw new IllegalStateException("XMLParser must be configured.");
        }
        String xacmlRequest = null;
        try {
            List<String> ctxPath = new ArrayList<>(1);
            ctxPath.add(ResponseType.class.getPackage().getName());
            ParserConfigurator configurator = parser.configureParser(ctxPath, BalanaClient.class.getClassLoader());
            configurator.addProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ObjectFactory objectFactory = new ObjectFactory();
            parser.marshal(configurator, objectFactory.createRequest(xacmlRequestType), os);
            xacmlRequest = os.toString("UTF-8");
        } catch (ParserException | UnsupportedEncodingException e) {
            String message = "Unable to marshal XACML request.";
            LOGGER.error(message, e);
            throw new PdpException(message, e);
        }

        LOGGER.debug("\nXACML 3.0 Request:\n{}", xacmlRequest);

        return xacmlRequest;
    }

    /**
     * Unmarshalls the XACML response.
     *
     * @param xacmlResponse The XACML response with all namespaces and namespace prefixes added.
     * @return The XACML response.
     * @throws PdpException
     */
    @SuppressWarnings("unchecked")
    private ResponseType unmarshal(DOMResult xacmlResponse) throws PdpException {
        List<String> ctxPath = ImmutableList.of(ResponseType.class.getPackage().getName());

        if (null == parser) {
            throw new IllegalStateException("XMLParser must be configured.");
        }

        ParserConfigurator configurator = parser.configureParser(ctxPath, BalanaClient.class.getClassLoader());

        try {
            JAXBElement<ResponseType> xacmlResponseTypeElement = parser.unmarshal(configurator, JAXBElement.class,
                    xacmlResponse.getNode());
            return xacmlResponseTypeElement.getValue();

        } catch (ParserException e) {
            String message = "Unable to unmarshal XACML response.";
            LOGGER.error(message);
            throw new PdpException(message, e);
        }
    }
}