Java tutorial
/** * 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; } }