org.phenotips.security.authorization.remote.internal.RemoteAuthorizationModule.java Source code

Java tutorial

Introduction

Here is the source code for org.phenotips.security.authorization.remote.internal.RemoteAuthorizationModule.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.phenotips.security.authorization.remote.internal;

import org.phenotips.data.Patient;
import org.phenotips.data.PatientRepository;
import org.phenotips.security.authorization.AuthorizationModule;

import org.xwiki.cache.Cache;
import org.xwiki.cache.CacheException;
import org.xwiki.cache.CacheFactory;
import org.xwiki.cache.config.LRUCacheConfiguration;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.security.authorization.Right;
import org.xwiki.users.User;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;

import net.sf.json.JSONObject;

/**
 * Rights checking class that respects the access level granted by a remote server.
 * <p>
 * The URL to access must be configured in {@code WEB-INF/xwiki.properties} under the
 * {@code phenotips.security.authorization.remote.url} key, for example:
 * </p>
 *
 * <pre>
 * phenotips.security.authorization.remote.url=http://emr.hospital.org/services/authorizationCheck
 * </pre>
 * <p>
 * PhenoTips sends a JSON payload, containing the following information:
 * </p>
 *
 * <pre>
 *   {
 *     "username": "someUserName",
 *     "access" : "view",
 *     "patient-id" : "P0123456",
 *     "patient-eid" : "PATIENT_1234"
 *   }
 * </pre>
 * <p>
 * The server must reply with a {@code 200 OK} response if the requested access is granted, or {@code 403 Forbidden} if
 * access is denied. Any other response is considered invalid, and the authorization check falls back to the default
 * PhenoTips rights checking.
 * </p>
 * <p>
 * By default, if no cache headers are received, the right is going to be cached for 1 minute. To disable caching, send
 * a {@code no-cache} or {@code no-store} {@code Cache-Control} header.
 * </p>
 * <p>
 * This module has priority 500.
 * </p>
 *
 * @version $Id: 8d24cf43fb0a37d2d1c56de92830a51bdc879bea $
 * @since 1.0M1
 */
@Component
@Named("remote-json")
@Singleton
public class RemoteAuthorizationModule implements AuthorizationModule, Initializable {
    /** The xwiki.properties key used to configure the remote server URL where authorization requests will be sent. */
    public static final String CONFIGURATION_KEY = "phenotips.security.authorization.remote.url";

    private static final byte GRANTED = 1;

    private static final byte DENIED = 2;

    private static final byte UNKNWON = 0;

    private static final byte ERROR = -1;

    /** Performs HTTP requests to the remote authorization server. */
    private final CloseableHttpClient client = HttpClients.createSystem();

    /** Logging helper object. */
    @Inject
    private Logger logger;

    @Inject
    @Named("infinispan")
    private CacheFactory factory;

    private Cache<Boolean> cache;

    @Inject
    @Named("restricted")
    private ConfigurationSource configuration;

    @Inject
    private PatientRepository patientRepository;

    private URI remoteServiceURL;

    @Override
    public int getPriority() {
        return 500;
    }

    @Override
    public Boolean hasAccess(User user, Right access, DocumentReference document) {
        if (user == null || access == null || document == null) {
            return null;
        }
        Patient patient = this.patientRepository.getPatientById(document.toString());
        if (patient == null) {
            return null;
        }
        String requestedRight = access.getName();
        String username = user.getUsername();
        String internalId = patient.getId();
        String externalId = patient.getExternalId();
        String cacheKey = getCacheKey(username, requestedRight, internalId);
        Boolean cachedAuthorization = this.cache.get(cacheKey);
        if (cachedAuthorization != null) {
            return cachedAuthorization;
        }
        Boolean result = null;
        byte decision = remoteCheck(requestedRight, username, internalId, externalId);
        if (decision == GRANTED) {
            result = Boolean.TRUE;
        } else if (decision == DENIED) {
            result = Boolean.FALSE;
        }
        return result;
    }

    @Override
    public void initialize() throws InitializationException {
        String configuredURL = this.configuration.getProperty(CONFIGURATION_KEY);
        if (StringUtils.isBlank(configuredURL)) {
            throw new InitializationException(this.getClass().getSimpleName()
                    + " requires a valid URL to be configured in xwiki.properties under the " + CONFIGURATION_KEY
                    + " key");
        }
        try {
            this.remoteServiceURL = new URI(configuredURL);
        } catch (URISyntaxException ex) {
            throw new InitializationException(
                    "Invalid URL configured for " + this.getClass().getSimpleName() + ": " + configuredURL, ex);
        }
        LRUCacheConfiguration config = new LRUCacheConfiguration("RemoteAuthorizationService", 1000, 60);
        try {
            this.cache = this.factory.newCache(config);
        } catch (CacheException ex) {
            throw new InitializationException("Failed to create authorization cache: " + ex.getMessage(), ex);
        }
    }

    private byte remoteCheck(String right, String username, String internalId, String externalId) {
        HttpPost method = new HttpPost(this.remoteServiceURL);

        JSONObject payload = new JSONObject();
        payload.element("access", right);
        payload.element("username", username);
        payload.element("patient-id", internalId);
        payload.element("patient-eid", externalId);
        method.setEntity(new StringEntity(payload.toString(), ContentType.APPLICATION_JSON));
        CloseableHttpResponse response = null;
        try {
            response = this.client.execute(method);
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                cacheResponse(getCacheKey(username, right, internalId), Boolean.TRUE, response);
                return GRANTED;
            } else if (response.getStatusLine().getStatusCode() == HttpStatus.SC_FORBIDDEN) {
                cacheResponse(getCacheKey(username, right, internalId), Boolean.FALSE, response);
                return DENIED;
            }
        } catch (IOException ex) {
            this.logger.warn("Failed to communicate with the authorization server: {}", ex.getMessage(), ex);
            return ERROR;
        } finally {
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e) {
                    // Just ignore, this shouldn't happen
                }
            }
        }
        return UNKNWON;
    }

    private void cacheResponse(String cacheKey, Boolean value, HttpResponse response) {
        Header cacheHeader = response.getLastHeader("Cache-Control");
        if (cacheHeader != null) {
            for (HeaderElement cacheSetting : cacheHeader.getElements()) {
                if (StringUtils.equals("no-cache", cacheSetting.getName())
                        || StringUtils.equals("no-store", cacheSetting.getName())) {
                    this.cache.remove(cacheKey);
                    return;
                }
            }
        }

        this.cache.set(cacheKey, value);
    }

    private String getCacheKey(String username, String right, String patientId) {
        return MessageFormat.format("{0}::{1}::{2}", username, right, patientId);
    }
}