com.kibana.multitenancy.plugin.acl.DynamicACLFilter.java Source code

Java tutorial

Introduction

Here is the source code for com.kibana.multitenancy.plugin.acl.DynamicACLFilter.java

Source

/**
 * Copyright (C) 2015 Distributed Search, Inc.
 *
 * 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.
 */
package com.kibana.multitenancy.plugin.acl;

import static com.kibana.multitenancy.plugin.kibana.KibanaSeed.setDashboards;

import com.kibana.multitenancy.plugin.ConfigurationSettings;
import com.kibana.multitenancy.plugin.kibana.KibanaSeed;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.engine.DocumentMissingException;
import org.elasticsearch.index.IndexNotFoundException;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestFilter;
import org.elasticsearch.rest.RestFilterChain;
import org.elasticsearch.rest.RestRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * REST filter to update the ACL when a user
 * first makes a request
 *
 */
public class DynamicACLFilter extends RestFilter implements ConfigurationSettings, ArmorACLActionRequestListener {

    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String SEARCHGUARD_TYPE = "ac";
    private static final String SEARCHGUARD_ID = "ac";

    private final ObjectMapper mapper = new ObjectMapper();
    private final ESLogger logger;
    private final UserProjectCache cache;
    private final String proxyUserHeader;
    private final Client esClient;
    private final String searchGuardIndex;
    private final String kibanaIndex;
    private final String kibanaVersion;
    private final int aclSyncDelay;
    private final String userProfilePrefix;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition syncing = lock.newCondition();
    private final String armorIndexStore;
    private Boolean seeded;

    @Inject
    public DynamicACLFilter(final UserProjectCache cache, final Settings settings, final Client client,
            final ACLNotifierService notifierService) {
        this.cache = cache;
        this.logger = Loggers.getLogger(getClass(), settings);
        this.esClient = client;
        this.proxyUserHeader = settings.get(ARMOR_AUTHENTICATION_PROXY_HEADER, DEFAULT_AUTH_PROXY_HEADER);
        this.searchGuardIndex = settings.get(ARMOR_CONFIG_INDEX_NAME, DEFAULT_SECURITY_CONFIG_INDEX);
        this.aclSyncDelay = Integer
                .valueOf(settings.get(ES_ACL_DELAY_IN_MILLIS, String.valueOf(DEFAULT_ES_ACL_DELAY)));
        this.userProfilePrefix = settings.get(ES_USER_PROFILE_PREFIX, DEFAULT_USER_PROFILE_PREFIX);
        this.kibanaIndex = settings.get(KIBANA_CONFIG_INDEX_NAME, DEFAULT_USER_PROFILE_PREFIX);
        this.kibanaVersion = settings.get(KIBANA_CONFIG_VERSION, DEFAULT_KIBANA_VERSION);
        this.armorIndexStore = settings.get(ConfigurationSettings.ARMOR_ACL_STORE_TYPE,
                ConfigurationSettings.ELASTIC);
        if (ConfigurationSettings.ELASTIC.equalsIgnoreCase(armorIndexStore)) {
            notifierService.addActionRequestListener(this);
            logger.debug("searchGuardIndex: {}", this.searchGuardIndex);
        }
        this.seeded = false;
    }

    public void onArmorACLActionRequest(String method) {
        logger.debug("Received notification that SearchGuard ACL was loaded");
        lock.lock();
        try {
            //seed initial ACL here
            if (!seeded) {
                try {
                    seedInitialACL(esClient);
                } catch (Exception e) {
                    logger.error("Exception encountered when seeding initial ACL", e);
                }
            }
            //end seeding

            syncing.signalAll();
        } finally {
            lock.unlock();
        }
    }

    /*As Authentication happens in Armor so This Filter should run at the end after ARMOR 
     * using the order function*/
    @Override
    public void process(RestRequest request, RestChannel channel, RestFilterChain chain) throws Exception {
        try {
            final String user = getUser(request);
            //final String token = getBearerToken(request);
            if (logger.isDebugEnabled()) {
                logger.debug("Handling Request...");
                logger.debug("Evaluating request for user '{}'", user);
                logger.debug("Cache has user: {}", cache.hasUser(user));
            }
            if (StringUtils.isNotEmpty(user) && !cache.hasUser(user)) {
                //Sushant : Removing Kubernetes Dependencies
                //final boolean isClusterAdmin = isClusterAdmin(token);
                final boolean isClusterAdmin = true;
                if (isClusterAdmin) {
                    request.putInContext(IAAS_ROLES, "cluster-admin");
                }
                if (updateCache(user, null, isClusterAdmin)) {
                    if (ConfigurationSettings.ELASTIC.equalsIgnoreCase(armorIndexStore)) {
                        syncAcl();
                    }
                }

            }

        } catch (Exception e) {
            logger.error("Error handling request in {}", e, this.getClass().getSimpleName());
        } finally {
            chain.continueProcessing(request, channel);
        }
    }

    /*As Authentication happens in Armor so This Filter should run at the end after ARMOR 
     * using the order function*/
    private String getUser(RestRequest request) {

        //Sushant:Getting user in case of Basic Authentication
        String username = "";
        //Sushant:Scenario when user Authenticated at Proxy level itself
        String proxyAuthUser = (String) ObjectUtils.defaultIfNull(request.header(proxyUserHeader), "");
        //Sushant: Scenario when user Authenticated at Proxy level itself

        String basicAuthorizationHeader = StringUtils.defaultIfBlank(request.header("Authorization"), "");
        if (StringUtils.isNotEmpty(basicAuthorizationHeader)) {
            String decodedBasicHeader = new String(
                    DatatypeConverter.parseBase64Binary(basicAuthorizationHeader.split(" ")[1]),
                    StandardCharsets.US_ASCII);
            final String[] decodedBasicHeaderParts = decodedBasicHeader.split(":");
            username = decodedBasicHeaderParts[0];
            decodedBasicHeader = null;
            basicAuthorizationHeader = null;
            logger.debug("User '{}' is authenticated", username);
        }
        return username;

    }

    private String getBearerToken(RestRequest request) {
        final String[] auth = ((String) ObjectUtils.defaultIfNull(request.header(AUTHORIZATION_HEADER), ""))
                .split(" ");
        if (auth.length >= 2 && "Bearer".equals(auth[0])) {
            return auth[1];
        }
        return "";
    }

    private boolean updateCache(final String user, final String token, final boolean isClusterAdmin) {
        logger.debug("Updating the cache for user '{}'", user);
        try {
            //Sushant: Updating cache with User-ProjectMapping , 1 user can have multiple projects. 
            //Note: In openstack it is yet to be decided 
            Set<String> projects = listProjectsFor(token);
            cache.update(user, projects, isClusterAdmin);

            Set<String> roles = new HashSet<String>();
            if (isClusterAdmin)
                roles.add("cluster-admin");

            KibanaSeed.setDashboards(user, projects, roles, esClient, kibanaIndex, kibanaVersion);
        } catch (Exception e) {
            logger.error("Error retrieving project list for '{}'", e, user);
            return false;
        }
        return true;
    }

    private Set<String> listProjectsFor(final String token) throws Exception {
        /*If Index is built with project name appended then add the project name 
         * in below hashset */
        Set<String> names = new HashSet<>();
        return names;
    }

    private synchronized void syncAcl() {
        logger.debug("Syncing the ACL to ElasticSearch");
        try {
            logger.debug("Loading SearchGuard ACL...");
            final ArmorACL acl = loadAcl(esClient);
            logger.debug("Syncing from cache to ACL...");
            acl.syncFrom(cache, userProfilePrefix);
            write(esClient, acl);
        } catch (Exception e) {
            logger.error("Exception while syncing ACL with cache", e);
        }
    }

    private ArmorACL loadAcl(Client esClient) throws IOException {
        GetRequest request = esClient.prepareGet(searchGuardIndex, SEARCHGUARD_TYPE, SEARCHGUARD_ID)
                .setRefresh(true).request();
        request.putInContext(ES_REQ_ID, ACL_FILTER_ID);
        GetResponse response = esClient.get(request).actionGet(); // need to worry about timeout?
        return mapper.readValue(response.getSourceAsBytes(), ArmorACL.class);
    }

    private void write(Client esClient, ArmorACL acl) throws JsonProcessingException, InterruptedException {
        if (logger.isDebugEnabled()) {
            logger.debug("Writing ACLs '{}'", mapper.writer(new DefaultPrettyPrinter()).writeValueAsString(acl));
        }
        UpdateRequest request = esClient.prepareUpdate(searchGuardIndex, SEARCHGUARD_TYPE, SEARCHGUARD_ID)
                .setDoc(mapper.writeValueAsBytes(acl)).setRefresh(true).request();
        request.putInContext(ES_REQ_ID, ACL_FILTER_ID);
        esClient.update(request).actionGet();
        lock.lock();
        try {
            logger.debug("Waiting up to {} ms. to be notified that SearchGuard has refreshed the ACLs",
                    aclSyncDelay);
            syncing.await(aclSyncDelay, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            logger.error("Error while awaiting notification of ACL load by SearchGuard", e);
        } finally {
            lock.unlock();
        }
    }

    private void create(Client esClient, ArmorACL acl) throws JsonProcessingException, InterruptedException {
        if (logger.isDebugEnabled()) {
            logger.debug("Writing ACLs '{}'", mapper.writer(new DefaultPrettyPrinter()).writeValueAsString(acl));
        }
        IndexRequest request = esClient.prepareIndex(searchGuardIndex, SEARCHGUARD_TYPE, SEARCHGUARD_ID)
                .setSource(mapper.writeValueAsBytes(acl)).setRefresh(true).request();
        request.putInContext(ES_REQ_ID, ACL_FILTER_ID);
        esClient.index(request).actionGet();
        lock.lock();
        try {
            logger.debug("Waiting up to {} ms. to be notified that SearchGuard has refreshed the ACLs",
                    aclSyncDelay);
            syncing.await(aclSyncDelay, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            logger.error("Error while awaiting notification of ACL load by SearchGuard", e);
        } finally {
            lock.unlock();
        }
    }

    private void seedInitialACL(Client esClient) throws Exception {

        ArmorACL acl = new ArmorACL();
        boolean create = false;

        try {
            //This should return nothing initially - if it does, we're done
            acl = loadAcl(esClient);

            if (acl.iterator().hasNext()) {
                if (logger.isDebugEnabled())
                    logger.debug("Have already seeded with '{}'",
                            mapper.writer(new DefaultPrettyPrinter()).writeValueAsString(acl));
                seeded = true;
                return;
            }
        }
        //Sushant:ElasticSearch 2.0 Breaking Changes
        //catch (IndexMissingException | DocumentMissingException | NullPointerException e) {
        catch (IndexNotFoundException | DocumentMissingException | NullPointerException e) {
            logger.debug("Caught Exception, ACL has not been seeded yet", e);
            create = true;
        } catch (Exception e) {
            logger.error("Error checking ACL when seeding", e);
            throw e;
        }

        try {
            acl.createInitialACLs();
            if (logger.isDebugEnabled())
                logger.debug("Created initial ACL of '{}'",
                        mapper.writer(new DefaultPrettyPrinter()).writeValueAsString(acl));

            if (create)
                create(esClient, acl);
            else
                write(esClient, acl);
        } catch (Exception e) {
            logger.error("Error seeding initial ACL", e);
            throw e;
        }

        seeded = true;
    }

    @Override
    public int order() {
        // need to run after armor
        return Integer.MAX_VALUE;
    }

}