| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with this |
| * work for additional information regarding copyright ownership. The ASF |
| * licenses this file to you 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 |
| * <p> |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * <p> |
| * 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 org.apache.knox.gateway.topology.monitor; |
| |
| import org.apache.commons.io.FileUtils; |
| import org.apache.commons.io.FilenameUtils; |
| import org.apache.knox.gateway.GatewayMessages; |
| import org.apache.knox.gateway.config.GatewayConfig; |
| import org.apache.knox.gateway.i18n.messages.MessagesFactory; |
| import org.apache.knox.gateway.services.ServiceLifecycleException; |
| import org.apache.knox.gateway.services.config.client.RemoteConfigurationRegistryClient; |
| import org.apache.knox.gateway.services.config.client.RemoteConfigurationRegistryClient.ChildEntryListener; |
| import org.apache.knox.gateway.services.config.client.RemoteConfigurationRegistryClient.EntryListener; |
| import org.apache.knox.gateway.services.config.client.RemoteConfigurationRegistryClientService; |
| import org.apache.zookeeper.ZooDefs; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| |
| class ZkRemoteConfigurationMonitorService implements RemoteConfigurationMonitor { |
| |
| private static final String NODE_KNOX = "/knox"; |
| private static final String NODE_KNOX_CONFIG = NODE_KNOX + "/config"; |
| private static final String NODE_KNOX_PROVIDERS = NODE_KNOX_CONFIG + "/shared-providers"; |
| private static final String NODE_KNOX_DESCRIPTORS = NODE_KNOX_CONFIG + "/descriptors"; |
| |
| private static GatewayMessages log = MessagesFactory.get(GatewayMessages.class); |
| |
| // N.B. This is ZooKeeper-specific, and should be abstracted when another registry is supported |
| private static final RemoteConfigurationRegistryClient.EntryACL AUTHENTICATED_USERS_ALL; |
| static { |
| AUTHENTICATED_USERS_ALL = new RemoteConfigurationRegistryClient.EntryACL() { |
| @Override |
| public String getId() { |
| return ""; |
| } |
| |
| @Override |
| public String getType() { |
| return "auth"; |
| } |
| |
| @Override |
| public Object getPermissions() { |
| return ZooDefs.Perms.ALL; |
| } |
| |
| @Override |
| public boolean canRead() { |
| return true; |
| } |
| |
| @Override |
| public boolean canWrite() { |
| return true; |
| } |
| }; |
| } |
| |
| private static final RemoteConfigurationRegistryClient.EntryACL WORLD_ANYONE_READ; |
| static { |
| WORLD_ANYONE_READ = new RemoteConfigurationRegistryClient.EntryACL() { |
| @Override |
| public String getId() { |
| return "anyone"; |
| } |
| |
| @Override |
| public String getType() { |
| return "world"; |
| } |
| |
| @Override |
| public Object getPermissions() { |
| return ZooDefs.Perms.READ; |
| } |
| |
| @Override |
| public boolean canRead() { |
| return true; |
| } |
| |
| @Override |
| public boolean canWrite() { |
| return false; |
| } |
| }; |
| } |
| |
| private RemoteConfigurationRegistryClient client; |
| |
| private File providersDir; |
| private File descriptorsDir; |
| |
| private final List<RemoteConfigurationRegistryClient.EntryACL> replacementACL = new ArrayList<>(); |
| |
| /** |
| * @param config The gateway configuration |
| * @param registryClientService The service from which the remote registry client should be acquired. |
| */ |
| ZkRemoteConfigurationMonitorService(GatewayConfig config, |
| RemoteConfigurationRegistryClientService registryClientService) { |
| this.providersDir = new File(config.getGatewayProvidersConfigDir()); |
| this.descriptorsDir = new File(config.getGatewayDescriptorsDir()); |
| |
| if (registryClientService != null) { |
| String clientName = config.getRemoteConfigurationMonitorClientName(); |
| if (clientName != null) { |
| this.client = registryClientService.get(clientName); |
| if (this.client == null) { |
| log.unresolvedClientConfigurationForRemoteMonitoring(clientName); |
| } else if (config.allowUnauthenticatedRemoteRegistryReadAccess()) { |
| replacementACL.add(WORLD_ANYONE_READ); |
| } |
| } else { |
| log.missingClientConfigurationForRemoteMonitoring(); |
| throw new IllegalStateException("Missing required configuration."); |
| } |
| } |
| |
| replacementACL.add(AUTHENTICATED_USERS_ALL); |
| } |
| |
| @Override |
| public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {} |
| |
| @Override |
| public void start() throws ServiceLifecycleException { |
| if (client == null) { |
| throw new IllegalStateException("Failed to acquire a remote configuration registry client."); |
| } |
| |
| final String monitorSource = client.getAddress(); |
| log.startingRemoteConfigurationMonitor(monitorSource); |
| |
| // Ensure the existence of the expected entries and their associated ACLs |
| ensureEntries(); |
| |
| // Confirm access to the remote provider configs directory znode |
| List<String> providerConfigs = client.listChildEntries(NODE_KNOX_PROVIDERS); |
| if (providerConfigs == null) { |
| // Either the ZNode does not exist, or there is an authentication problem |
| throw new IllegalStateException("Unable to access remote path: " + NODE_KNOX_PROVIDERS); |
| } else { |
| // Download any existing provider configs in the remote registry, which either do not exist locally, or have |
| // been modified, so that they are certain to be present when this monitor downloads any descriptors that |
| // reference them. |
| for (String providerConfig : providerConfigs) { |
| File localFile = new File(providersDir, providerConfig); |
| |
| try { |
| byte[] remoteContent = client.getEntryData(NODE_KNOX_PROVIDERS + "/" + providerConfig).getBytes(StandardCharsets.UTF_8); |
| if (!localFile.exists() || !Arrays.equals(remoteContent, FileUtils.readFileToByteArray(localFile))) { |
| FileUtils.writeByteArrayToFile(localFile, remoteContent); |
| log.downloadedRemoteConfigFile(providersDir.getName(), providerConfig); |
| } |
| } catch (IOException e) { |
| throw new ServiceLifecycleException("Exception while downloading remote configs from zookeeper", e); |
| } |
| } |
| } |
| |
| // Confirm access to the remote descriptors directory znode |
| List<String> descriptors = client.listChildEntries(NODE_KNOX_DESCRIPTORS); |
| if (descriptors == null) { |
| // Either the ZNode does not exist, or there is an authentication problem |
| throw new IllegalStateException("Unable to access remote path: " + NODE_KNOX_DESCRIPTORS); |
| } |
| |
| try { |
| // Register a listener for provider config znode additions/removals |
| client.addChildEntryListener(NODE_KNOX_PROVIDERS, new ConfigDirChildEntryListener(providersDir)); |
| |
| // Register a listener for descriptor znode additions/removals |
| client.addChildEntryListener(NODE_KNOX_DESCRIPTORS, new ConfigDirChildEntryListener(descriptorsDir)); |
| } catch (Exception e) { |
| throw new ServiceLifecycleException("Exception while registering provider/descriptor znode listeners", e); |
| } |
| |
| log.monitoringRemoteConfigurationSource(monitorSource); |
| } |
| |
| |
| @Override |
| public void stop() throws ServiceLifecycleException { |
| try { |
| client.removeEntryListener(NODE_KNOX_PROVIDERS); |
| client.removeEntryListener(NODE_KNOX_DESCRIPTORS); |
| } catch (Exception e) { |
| throw new ServiceLifecycleException("Exception while stopping: " + getClass().getName(), e); |
| } |
| } |
| |
| @Override |
| public boolean createProvider(String name, String content) { |
| String entryPath = "/knox/config/shared-providers/" + name; |
| client.createEntry(entryPath, content); |
| return (client.getEntryData(entryPath) != null); |
| } |
| |
| @Override |
| public boolean createDescriptor(String name, String content) { |
| String entryPath = "/knox/config/descriptors/" + name; |
| client.createEntry(entryPath, content); |
| return (client.getEntryData(entryPath) != null); |
| } |
| |
| @Override |
| public boolean deleteProvider(String name) { |
| return deleteEntry("/knox/config/descriptors", name); |
| } |
| |
| @Override |
| public boolean deleteDescriptor(String name) { |
| return deleteEntry("/knox/config/shared-providers", name); |
| } |
| |
| private boolean deleteEntry(String entryParent, String name) { |
| boolean result = false; |
| List<String> existingProviderConfigs = client.listChildEntries(entryParent); |
| for (String entryName : existingProviderConfigs) { |
| if (FilenameUtils.getName(entryName).equals(name)) { |
| String entryPath = entryParent + "/" + entryName; |
| client.deleteEntry(entryPath); |
| result = !client.entryExists(entryPath); |
| if (!result) { |
| log.failedToDeletedRemoteConfigFile("descriptor", name); |
| } |
| break; |
| } |
| } |
| return result; |
| } |
| |
| private void ensureEntries() { |
| ensureEntry(NODE_KNOX); |
| ensureEntry(NODE_KNOX_CONFIG); |
| ensureEntry(NODE_KNOX_PROVIDERS); |
| ensureEntry(NODE_KNOX_DESCRIPTORS); |
| } |
| |
| private void ensureEntry(String name) { |
| if (!client.entryExists(name)) { |
| client.createEntry(name); |
| } else { |
| // Validate the ACL |
| List<RemoteConfigurationRegistryClient.EntryACL> entryACLs = client.getACL(name); |
| for (RemoteConfigurationRegistryClient.EntryACL entryACL : entryACLs) { |
| // N.B. This is ZooKeeper-specific, and should be abstracted when another registry is supported |
| // For now, check for ZooKeeper world:anyone with ANY permissions (even read-only) |
| if (entryACL.getType().equals("world") && entryACL.getId().equals("anyone")) { |
| log.suspectWritableRemoteConfigurationEntry(name); |
| |
| // If the client is authenticated, but "anyone" can write the content, then the content may not |
| // be trustworthy. |
| if (client.isAuthenticationConfigured()) { |
| log.correctingSuspectWritableRemoteConfigurationEntry(name); |
| |
| // Replace the existing ACL with the replacement ACL for the authentication scenario |
| client.setACL(name, replacementACL); |
| } |
| } |
| } |
| } |
| } |
| |
| private static class ConfigDirChildEntryListener implements ChildEntryListener { |
| File localDir; |
| |
| ConfigDirChildEntryListener(File localDir) { |
| this.localDir = localDir; |
| } |
| |
| @Override |
| public void childEvent(RemoteConfigurationRegistryClient client, Type type, String path) { |
| File localFile = new File(localDir, path.substring(path.lastIndexOf('/') + 1)); |
| |
| switch (type) { |
| case REMOVED: |
| FileUtils.deleteQuietly(localFile); |
| log.deletedRemoteConfigFile(localDir.getName(), localFile.getName()); |
| try { |
| client.removeEntryListener(path); |
| } catch (Exception e) { |
| log.errorRemovingRemoteConfigurationListenerForPath(path, e); |
| } |
| break; |
| case ADDED: |
| try { |
| client.addEntryListener(path, new ConfigEntryListener(localDir)); |
| } catch (Exception e) { |
| log.errorAddingRemoteConfigurationListenerForPath(path, e); |
| } |
| break; |
| } |
| } |
| } |
| |
| private static class ConfigEntryListener implements EntryListener { |
| private File localDir; |
| |
| ConfigEntryListener(File localDir) { |
| this.localDir = localDir; |
| } |
| |
| @Override |
| public void entryChanged(RemoteConfigurationRegistryClient client, String path, byte[] data) { |
| File localFile = new File(localDir, path.substring(path.lastIndexOf('/'))); |
| if (data != null) { |
| try { |
| // If there is no corresponding local file, or the content is different from the existing local |
| // file, write the data to the local file. |
| if (!localFile.exists() || !Arrays.equals(FileUtils.readFileToByteArray(localFile), data)) { |
| FileUtils.writeByteArrayToFile(localFile, data); |
| log.downloadedRemoteConfigFile(localDir.getName(), localFile.getName()); |
| } |
| } catch (IOException e) { |
| log.errorDownloadingRemoteConfiguration(path, e); |
| } |
| } else { |
| FileUtils.deleteQuietly(localFile); |
| log.deletedRemoteConfigFile(localDir.getName(), localFile.getName()); |
| } |
| } |
| } |
| |
| } |