blob: c4de38d0ff0971095a6d52f301078c72efe9327c [file] [log] [blame]
/*
* 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
*
* 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 org.apache.knox.gateway.util;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.knox.gateway.GatewayCommandLine;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.config.impl.GatewayConfigImpl;
import org.apache.knox.gateway.deploy.DeploymentFactory;
import org.apache.knox.gateway.services.CLIGatewayServices;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.services.Service;
import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.config.client.RemoteConfigurationRegistryClient;
import org.apache.knox.gateway.services.config.client.RemoteConfigurationRegistryClientService;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.gateway.services.security.MasterService;
import org.apache.knox.gateway.services.topology.TopologyService;
import org.apache.knox.gateway.topology.Provider;
import org.apache.knox.gateway.topology.Topology;
import org.apache.knox.gateway.topology.validation.TopologyValidator;
import org.apache.log4j.PropertyConfigurator;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.config.Ini;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.apache.shiro.util.ThreadContext;
import org.eclipse.persistence.oxm.MediaType;
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter;
import org.jboss.shrinkwrap.api.spec.EnterpriseArchive;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import java.io.BufferedReader;
import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
public class KnoxCLI extends Configured implements Tool {
private static final String USAGE_PREFIX = "KnoxCLI {cmd} [options]";
private static final String COMMANDS =
" [--help]\n" +
" [" + VersionCommand.USAGE + "]\n" +
" [" + MasterCreateCommand.USAGE + "]\n" +
" [" + CertCreateCommand.USAGE + "]\n" +
" [" + CertExportCommand.USAGE + "]\n" +
" [" + AliasCreateCommand.USAGE + "]\n" +
" [" + AliasDeleteCommand.USAGE + "]\n" +
" [" + AliasListCommand.USAGE + "]\n" +
" [" + RedeployCommand.USAGE + "]\n" +
" [" + ListTopologiesCommand.USAGE + "]\n" +
" [" + ValidateTopologyCommand.USAGE + "]\n" +
" [" + LDAPAuthCommand.USAGE + "]\n" +
" [" + LDAPSysBindCommand.USAGE + "]\n" +
" [" + ServiceTestCommand.USAGE + "]\n" +
" [" + RemoteRegistryClientsListCommand.USAGE + "]\n" +
" [" + RemoteRegistryListProviderConfigsCommand.USAGE + "]\n" +
" [" + RemoteRegistryUploadProviderConfigCommand.USAGE + "]\n" +
" [" + RemoteRegistryListDescriptorsCommand.USAGE + "]\n" +
" [" + RemoteRegistryUploadDescriptorCommand.USAGE + "]\n" +
" [" + RemoteRegistryDeleteProviderConfigCommand.USAGE + "]\n" +
" [" + RemoteRegistryDeleteDescriptorCommand.USAGE + "]\n" +
" [" + RemoteRegistryGetACLCommand.USAGE + "]\n";
/** allows stdout to be captured if necessary */
public PrintStream out = System.out;
/** allows stderr to be captured if necessary */
public PrintStream err = System.err;
private static GatewayServices services = new CLIGatewayServices();
private Command command;
private String value;
private String cluster;
private String path;
private String generate = "false";
private String hostname;
private String port;
private boolean force;
private boolean debug;
private String user;
private String pass;
private boolean groups;
private String remoteRegistryClient;
private String remoteRegistryEntryName;
// For testing only
private String master;
private String type;
@Override
public int run(String[] args) throws Exception {
int exitCode = 0;
try {
exitCode = init(args);
if (exitCode != 0) {
return exitCode;
}
if (command != null && command.validate()) {
initializeServices( command instanceof MasterCreateCommand );
command.execute();
} else if (!(command instanceof MasterCreateCommand)){
out.println("ERROR: Invalid Command" + "\n" + "Unrecognized option:" +
args[0] + "\n" +
"A fatal exception has occurred. Program will exit.");
exitCode = -2;
}
} catch (ServiceLifecycleException sle) {
out.println("ERROR: Internal Error: Please refer to the knoxcli.log " +
"file for details. " + sle.getMessage());
} catch (Exception e) {
e.printStackTrace( err );
err.flush();
return -3;
}
return exitCode;
}
public static synchronized GatewayServices getGatewayServices() {
return services;
}
private void initializeServices(boolean persisting) throws ServiceLifecycleException {
GatewayConfig config = getGatewayConfig();
Map<String,String> options = new HashMap<>();
options.put(GatewayCommandLine.PERSIST_LONG, Boolean.toString(persisting));
if (master != null) {
options.put("master", master);
}
services.init(config, options);
}
/**
* Parse the command line arguments and initialize the data.
*
* See the usage information for the commands itself for information on which parameters they take.
*
* @param args command line arguments
* @return return exit code
* @throws IOException exception on starting KnoxCLI
*/
private int init(String[] args) throws IOException {
if (args.length == 0) {
printKnoxShellUsage();
return -1;
}
for (int i = 0; i < args.length; i++) { // parse command line
if (args[i].equals("create-master")) {
command = new MasterCreateCommand();
if ((args.length > i + 1) && args[i + 1].equals("--help")) {
printKnoxShellUsage();
return -1;
}
} else if (args[i].equals("delete-alias")) {
String alias = null;
if (args.length >= 2) {
alias = args[++i];
}
command = new AliasDeleteCommand(alias);
if (alias == null || "--help".equals(alias)) {
printKnoxShellUsage();
return -1;
}
} else if (args[i].equals("create-alias")) {
String alias = null;
if (args.length >= 2) {
alias = args[++i];
}
command = new AliasCreateCommand(alias);
if (alias == null || "--help".equals(alias)) {
printKnoxShellUsage();
return -1;
}
} else if (args[i].equals("create-cert")) {
command = new CertCreateCommand();
if ((args.length > i + 1) && args[i + 1].equals("--help")) {
printKnoxShellUsage();
return -1;
}
} else if (args[i].equals("export-cert")) {
command = new CertExportCommand();
if ((args.length > i + 1) && args[i + 1].equals("--help")) {
printKnoxShellUsage();
return -1;
}
}else if(args[i].equals("user-auth-test")) {
if(i + 1 >= args.length) {
printKnoxShellUsage();
return -1;
} else {
command = new LDAPAuthCommand();
}
} else if(args[i].equals("system-user-auth-test")) {
if (i + 1 >= args.length){
printKnoxShellUsage();
return -1;
} else {
command = new LDAPSysBindCommand();
}
} else if (args[i].equals("list-alias")) {
command = new AliasListCommand();
} else if (args[i].equals("--value")) {
if( i+1 >= args.length || args[i+1].startsWith( "-" ) ) {
printKnoxShellUsage();
return -1;
}
this.value = args[++i];
if ( command instanceof MasterCreateCommand ) {
this.master = this.value;
}
} else if ( args[i].equals("version") ) {
command = new VersionCommand();
} else if ( args[i].equals("redeploy") ) {
command = new RedeployCommand();
} else if ( args[i].equals("validate-topology") ) {
if(i + 1 >= args.length) {
printKnoxShellUsage();
return -1;
} else {
command = new ValidateTopologyCommand();
}
} else if( args[i].equals("list-topologies") ){
command = new ListTopologiesCommand();
}else if ( args[i].equals("--cluster") || args[i].equals("--topology") ) {
if( i+1 >= args.length || args[i+1].startsWith( "-" ) ) {
printKnoxShellUsage();
return -1;
}
this.cluster = args[++i];
} else if (args[i].equals("service-test")) {
if( i + 1 >= args.length) {
printKnoxShellUsage();
return -1;
} else {
command = new ServiceTestCommand();
}
} else if (args[i].equals("--generate")) {
if ( command instanceof MasterCreateCommand ) {
this.master = UUID.randomUUID().toString();
} else {
this.generate = "true";
}
} else if(args[i].equals("--type")) {
if( i+1 >= args.length || args[i+1].startsWith( "-" ) ) {
printKnoxShellUsage();
return -1;
}
this.type = args[++i];
} else if(args[i].equals("--path")) {
if( i+1 >= args.length || args[i+1].startsWith( "-" ) ) {
printKnoxShellUsage();
return -1;
}
this.path = args[++i];
}else if (args[i].equals("--hostname")) {
if( i+1 >= args.length || args[i+1].startsWith( "-" ) ) {
printKnoxShellUsage();
return -1;
}
this.hostname = args[++i];
} else if (args[i].equals("--port")) {
if( i+1 >= args.length || args[i+1].startsWith( "-" ) ) {
printKnoxShellUsage();
return -1;
}
this.port = args[++i];
} else if (args[i].equals("--master")) {
// For testing only
if( i+1 >= args.length || args[i+1].startsWith( "-" ) ) {
printKnoxShellUsage();
return -1;
}
this.master = args[++i];
} else if (args[i].equals("--force")) {
this.force = true;
} else if (args[i].equals("--help")) {
printKnoxShellUsage();
return -1;
} else if(args[i].equals("--d")) {
this.debug = true;
} else if(args[i].equals("--u")) {
if(i + 1 <= args.length) {
this.user = args[++i];
} else{
printKnoxShellUsage();
return -1;
}
} else if(args[i].equals("--p")) {
if(i + 1 <= args.length) {
this.pass = args[++i];
} else{
printKnoxShellUsage();
return -1;
}
} else if (args[i].equals("--g")) {
this.groups = true;
} else if (args[i].equals("list-registry-clients")) {
command = new RemoteRegistryClientsListCommand();
} else if (args[i].equals("--registry-client")) {
if (i + 1 >= args.length || args[i + 1].startsWith("-")) {
printKnoxShellUsage();
return -1;
}
this.remoteRegistryClient = args[++i];
} else if (args[i].equalsIgnoreCase("list-provider-configs")) {
command = new RemoteRegistryListProviderConfigsCommand();
} else if (args[i].equalsIgnoreCase("list-descriptors")) {
command = new RemoteRegistryListDescriptorsCommand();
} else if (args[i].equalsIgnoreCase("upload-provider-config")) {
String fileName;
if (i <= (args.length - 1)) {
fileName = args[++i];
command = new RemoteRegistryUploadProviderConfigCommand(fileName);
} else {
printKnoxShellUsage();
return -1;
}
} else if (args[i].equals("upload-descriptor")) {
String fileName;
if (i <= (args.length - 1)) {
fileName = args[++i];
command = new RemoteRegistryUploadDescriptorCommand(fileName);
} else {
printKnoxShellUsage();
return -1;
}
} else if (args[i].equals("--entry-name")) {
if (i <= (args.length - 1)) {
remoteRegistryEntryName = args[++i];
} else {
printKnoxShellUsage();
return -1;
}
} else if (args[i].equals("delete-descriptor")) {
if (i <= (args.length - 1)) {
String entry = args[++i];
command = new RemoteRegistryDeleteDescriptorCommand(entry);
} else {
printKnoxShellUsage();
return -1;
}
} else if (args[i].equals("delete-provider-config")) {
if (i <= (args.length - 1)) {
String entry = args[++i];
command = new RemoteRegistryDeleteProviderConfigCommand(entry);
} else {
printKnoxShellUsage();
return -1;
}
} else if (args[i].equalsIgnoreCase("get-registry-acl")) {
if (i <= (args.length - 1)) {
String entry = args[++i];
command = new RemoteRegistryGetACLCommand(entry);
} else {
printKnoxShellUsage();
return -1;
}
} else {
printKnoxShellUsage();
//ToolRunner.printGenericCommandUsage(System.err);
return -1;
}
}
return 0;
}
private void printKnoxShellUsage() {
out.println( USAGE_PREFIX + "\n" + COMMANDS );
if ( command != null ) {
out.println(command.getUsage());
} else {
char[] chars = new char[79];
Arrays.fill( chars, '=' );
String div = new String( chars );
out.println( div );
out.println( VersionCommand.USAGE + "\n\n" + VersionCommand.DESC );
out.println();
out.println( div );
out.println( MasterCreateCommand.USAGE + "\n\n" + MasterCreateCommand.DESC );
out.println();
out.println( div );
out.println( CertCreateCommand.USAGE + "\n\n" + CertCreateCommand.DESC );
out.println();
out.println( div );
out.println( CertExportCommand.USAGE + "\n\n" + CertExportCommand.DESC );
out.println();
out.println( div );
out.println( AliasCreateCommand.USAGE + "\n\n" + AliasCreateCommand.DESC );
out.println();
out.println( div );
out.println( AliasDeleteCommand.USAGE + "\n\n" + AliasDeleteCommand.DESC );
out.println();
out.println( div );
out.println( AliasListCommand.USAGE + "\n\n" + AliasListCommand.DESC );
out.println();
out.println( div );
out.println( RedeployCommand.USAGE + "\n\n" + RedeployCommand.DESC );
out.println();
out.println( div );
out.println(ValidateTopologyCommand.USAGE + "\n\n" + ValidateTopologyCommand.DESC);
out.println();
out.println( div );
out.println(ListTopologiesCommand.USAGE + "\n\n" + ListTopologiesCommand.DESC);
out.println();
out.println( div );
out.println(LDAPAuthCommand.USAGE + "\n\n" + LDAPAuthCommand.DESC);
out.println();
out.println( div );
out.println(LDAPSysBindCommand.USAGE + "\n\n" + LDAPSysBindCommand.DESC);
out.println();
out.println( div );
out.println(ServiceTestCommand.USAGE + "\n\n" + ServiceTestCommand.DESC);
out.println();
out.println( div );
out.println(RemoteRegistryClientsListCommand.USAGE + "\n\n" + RemoteRegistryClientsListCommand.DESC);
out.println();
out.println( div );
out.println(RemoteRegistryGetACLCommand.USAGE + "\n\n" + RemoteRegistryGetACLCommand.DESC);
out.println();
out.println( div );
out.println(RemoteRegistryListProviderConfigsCommand.USAGE + "\n\n" + RemoteRegistryListProviderConfigsCommand.DESC);
out.println();
out.println( div );
out.println(RemoteRegistryListDescriptorsCommand.USAGE + "\n\n" + RemoteRegistryListDescriptorsCommand.DESC);
out.println();
out.println( div );
out.println(RemoteRegistryUploadProviderConfigCommand.USAGE + "\n\n" + RemoteRegistryUploadProviderConfigCommand.DESC);
out.println();
out.println( div );
out.println(RemoteRegistryUploadDescriptorCommand.USAGE + "\n\n" + RemoteRegistryUploadDescriptorCommand.DESC);
out.println();
out.println( div );
out.println(RemoteRegistryDeleteProviderConfigCommand.USAGE + "\n\n" + RemoteRegistryDeleteProviderConfigCommand.DESC);
out.println();
out.println( div );
out.println(RemoteRegistryDeleteDescriptorCommand.USAGE + "\n\n" + RemoteRegistryDeleteDescriptorCommand.DESC);
out.println();
out.println( div );
}
}
private abstract class Command {
public boolean validate() {
return true;
}
protected Service getService(String serviceName) {
return null;
}
public abstract void execute() throws Exception;
public abstract String getUsage();
protected AliasService getAliasService() {
return services.getService(ServiceType.ALIAS_SERVICE);
}
protected KeystoreService getKeystoreService() {
return services.getService(ServiceType.KEYSTORE_SERVICE);
}
protected TopologyService getTopologyService() {
return services.getService(ServiceType.TOPOLOGY_SERVICE);
}
protected RemoteConfigurationRegistryClientService getRemoteConfigRegistryClientService() {
return services.getService(ServiceType.REMOTE_REGISTRY_CLIENT_SERVICE);
}
}
private class AliasListCommand extends Command {
public static final String USAGE = "list-alias [--cluster clustername]";
public static final String DESC = "The list-alias command lists all of the aliases\n" +
"for the given hadoop --cluster. The default\n" +
"--cluster being the gateway itself.";
@Override
public void execute() throws Exception {
AliasService as = getAliasService();
KeystoreService keystoreService = getKeystoreService();
if (cluster == null) {
cluster = "__gateway";
}
boolean credentialStoreForClusterAvailable =
keystoreService.isCredentialStoreForClusterAvailable(cluster);
if (credentialStoreForClusterAvailable) {
out.println("Listing aliases for: " + cluster);
List<String> aliases = as.getAliasesForCluster(cluster);
for (String alias : aliases) {
out.println(alias);
}
out.println("\n" + aliases.size() + " items.");
} else {
out.println("Invalid cluster name provided: " + cluster);
}
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
public class CertExportCommand extends Command {
public static final String USAGE = "export-cert [--type PEM|JKS|JCEKS|PKCS12]";
public static final String DESC = "The export-cert command exports the public certificate\n" +
"from the a gateway.jks keystore with the alias of gateway-identity.\n" +
"It will be exported to `{GATEWAY_HOME}/data/security/keystores/` with a name of `gateway-client-trust.<type>`" +
"Using the --type option you can specify which keystore type you need (default: PEM)\n" +
"NOTE: The password for the JKS, JCEKS and PKCS12 types is `changeit`.\n" +
"It can be changed using: `keytool -storepasswd -storetype <type> -keystore gateway-client-trust.<type>`";
private GatewayConfig getGatewayConfig() {
GatewayConfig result;
Configuration conf = getConf();
if (conf instanceof GatewayConfig) {
result = (GatewayConfig) conf;
} else {
result = new GatewayConfigImpl();
}
return result;
}
@Override
public void execute() throws Exception {
KeystoreService ks = getKeystoreService();
if (ks != null) {
try {
if (!ks.isKeystoreForGatewayAvailable()) {
out.println("No keystore has been created for the gateway. Please use the create-cert command or populate with a CA signed cert of your own.");
}
GatewayConfig config = getGatewayConfig();
Certificate cert = ks.getKeystoreForGateway().getCertificate(config.getIdentityKeyAlias());
String keyStoreDir = config.getGatewayKeystoreDir() + File.separator;
File ksd = new File(keyStoreDir);
if (!ksd.exists()) {
if (!ksd.mkdirs()) {
// certainly should not happen if the keystore is known to be available
throw new ServiceLifecycleException("Unable to create keystores directory" + ksd.getAbsolutePath());
}
}
if ("PEM".equalsIgnoreCase(type) || type == null) {
X509CertificateUtil.writeCertificateToFile(cert, new File(keyStoreDir + "gateway-identity.pem"));
out.println("Certificate gateway-identity has been successfully exported to: " + keyStoreDir + "gateway-identity.pem");
} else if ("JKS".equalsIgnoreCase(type)) {
X509CertificateUtil.writeCertificateToJks(cert, new File(keyStoreDir + "gateway-client-trust.jks"));
out.println("Certificate gateway-identity has been successfully exported to: " + keyStoreDir + "gateway-client-trust.jks");
} else if ("JCEKS".equalsIgnoreCase(type)) {
X509CertificateUtil.writeCertificateToJceks(cert, new File(keyStoreDir + "gateway-client-trust.jceks"));
out.println("Certificate gateway-identity has been successfully exported to: " + keyStoreDir + "gateway-client-trust.jceks");
} else if ("PKCS12".equalsIgnoreCase(type)) {
X509CertificateUtil.writeCertificateToPkcs12(cert, new File(keyStoreDir + "gateway-client-trust.pkcs12"));
out.println("Certificate gateway-identity has been successfully exported to: " + keyStoreDir + "gateway-client-trust.pkcs12");
} else {
out.println("Invalid type for export file provided. Export has not been done. Please use: [PEM|JKS|JCEKS|PKCS12] default value is PEM.");
}
} catch (KeystoreServiceException e) {
throw new ServiceLifecycleException("The identity keystore was not loaded properly - the provided password may not match the password for the keystore.", e);
}
}
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
public class CertCreateCommand extends Command {
public static final String USAGE = "create-cert [--force] [--hostname h]";
public static final String DESC = "The create-cert command populates the configured identity\n" +
"keystore with a self-signed certificate to be used as the\n" +
"gateway identity. If a cert exists and it is determined to\n" +
"not have been generated by Knox, --force must be specified\n" +
"to overwrite it. If a self-signed cert is created, a\n" +
"password for the key will be generated and stored in the\n" +
"__gateway-credentials.jceks credential store.";
private static final String GATEWAY_CREDENTIAL_STORE_NAME = "__gateway";
public CertCreateCommand() {
}
@Override
public void execute() throws Exception {
KeystoreService ks = getKeystoreService();
AliasService as = getAliasService();
if (ks != null) {
try {
if (!ks.isCredentialStoreForClusterAvailable(GATEWAY_CREDENTIAL_STORE_NAME)) {
// log.creatingCredentialStoreForGateway();
ks.createCredentialStoreForCluster(GATEWAY_CREDENTIAL_STORE_NAME);
} else {
// log.credentialStoreForGatewayFoundNotCreating();
}
// LET'S NOT GENERATE A DIFFERENT KEY PASSPHRASE BY DEFAULT ANYMORE
// IF A DEPLOYMENT WANTS TO CHANGE THE KEY PASSPHRASE TO MAKE IT MORE SECURE THEN
// THEY CAN ADD THE ALIAS EXPLICITLY WITH THE CLI
//as.generateAliasForCluster(GATEWAY_CREDENTIAL_STORE_NAME, GATEWAY_IDENTITY_PASSPHRASE);
} catch (KeystoreServiceException e) {
throw new ServiceLifecycleException("Keystore was not loaded properly - the stored password may not match the password for the keystore.", e);
}
try {
if (!ks.isKeystoreForGatewayAvailable()) {
// log.creatingKeyStoreForGateway();
ks.createKeystoreForGateway();
}
else {
// log.keyStoreForGatewayFoundNotCreating();
}
GatewayConfig config = getGatewayConfig();
if ( !isForceRequired(config, ks) || force) {
char[] passphrase = as.getGatewayIdentityPassphrase();
if (passphrase == null) {
MasterService ms = services.getService(ServiceType.MASTER_SERVICE);
passphrase = ms.getMasterSecret();
}
ks.addSelfSignedCertForGateway(config.getIdentityKeyAlias(), passphrase, hostname);
// logAndValidateCertificate();
out.println("Certificate " + config.getIdentityKeyAlias() + " has been successfully created.");
} else {
// require --force to replace...
out.println("A non-self-signed certificate has already been installed in the configured keystore. " +
"Please use --force if you wish to overwrite it with a generated self-signed certificate.");
}
} catch (KeystoreServiceException e) {
throw new ServiceLifecycleException("The identity keystore was not loaded properly - the provided password may not match the password for the keystore.", e);
}
}
}
/**
* Determines if <code>--force</code> should be used inorder to not accidentally overwrite a
* real certificate.
* <p>
* <p>
* All of the following must be met for <code>--force</code> to <b>NOT</b> be required:
* <ul>
* <li>The path to the keystore file is the default path: <code>[Gateway Keystore Directory]/gateway.jks</code></li>
* <li>The alias name for the key is the default name: <code>gateway-identity</code></li>
* <li>The relevant certificate does not exist or is self-signed</li>
* <li>The relevant certificate has a subject name ending in "OU=Test,O=Hadoop,L=Test,ST=Test,C=US"</li>
* </ul>
*
* @param config the Gateway configuration
* @param ks a {@link KeystoreService} implementation
* @return <code>true</code> if <code>--force</code> is required; otherwise <code>false</code>
*/
private boolean isForceRequired(GatewayConfig config, KeystoreService ks) {
// Test the path of the keystore file
Path defaultKeystorePath = Paths.get(config.getGatewayKeystoreDir(), GatewayConfig.DEFAULT_GATEWAY_KEYSTORE_NAME).toAbsolutePath();
Path actualKeystorePath = Paths.get(config.getIdentityKeystorePath()).toAbsolutePath();
if (!defaultKeystorePath.equals(actualKeystorePath)) {
// The path is not the default path: --force is required
return true;
}
// Test the alias name for the key
if (!GatewayConfig.DEFAULT_IDENTITY_KEY_ALIAS.equals(config.getIdentityKeyAlias())) {
// The alias name for the key is not the default name (gateway-identity): --force is required
return true;
}
// Test the certificate
try {
Certificate certificate = ks.getCertificateForGateway();
if (certificate instanceof X509Certificate) {
if (!X509CertificateUtil.isSelfSignedCertificate(certificate)) {
// The relevant certificate exists and is not self-signed: --force is required
return true;
} else if (!((X509Certificate) certificate).getSubjectDN().getName().matches(".*?,\\s*OU=Test,\\s*O=Hadoop,\\s*L=Test,\\s*ST=Test,\\s*C=US")) {
// The subject name of certificate does not end with "OU=Test,O=Hadoop,L=Test,ST=Test,C=US": --force is required
return true;
}
}
} catch (KeyStoreException | KeystoreServiceException e) {
// A certificate was (probably) not previously created...
}
// All indicators point to a previously created test certificate: --force is not required
return false;
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
public class AliasCreateCommand extends Command {
public static final String USAGE = "create-alias aliasname [--cluster clustername] " +
"[ (--value v) | (--generate) ]";
public static final String DESC = "The create-alias command will create an alias\n"
+ "and secret pair within the credential store for the\n"
+ "indicated --cluster otherwise within the gateway\n"
+ "credential store. The actual secret may be specified via\n"
+ "the --value option or --generate (will create a random secret\n"
+ "for you) or user will be prompt to provide password.";
private String name;
public AliasCreateCommand(String alias) {
name = alias;
}
@Override
public void execute() throws Exception {
AliasService as = getAliasService();
if (cluster == null) {
cluster = "__gateway";
}
if (value != null) {
as.addAliasForCluster(cluster, name, value);
out.println(name + " has been successfully created.");
}
else {
if (Boolean.parseBoolean(generate)) {
as.generateAliasForCluster(cluster, name);
out.println(name + " has been successfully generated.");
}
else {
value = new String(promptUserForPassword());
as.addAliasForCluster(cluster, name, value);
out.println(name + " has been successfully created.");
}
}
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
protected char[] promptUserForPassword() {
char[] password = null;
Console c = System.console();
if (c == null) {
System.err
.println("No console to fetch password from user.Consider setting via --generate or --value.");
System.exit(1);
}
boolean noMatch;
do {
char[] newPassword1 = c.readPassword("Enter password: ");
char[] newPassword2 = c.readPassword("Enter password again: ");
noMatch = !Arrays.equals(newPassword1, newPassword2);
if (noMatch) {
c.format("Passwords don't match. Try again.%n");
} else {
password = Arrays.copyOf(newPassword1, newPassword1.length);
}
Arrays.fill(newPassword1, ' ');
Arrays.fill(newPassword2, ' ');
} while (noMatch);
return password;
}
}
public class AliasDeleteCommand extends Command {
public static final String USAGE = "delete-alias aliasname [--cluster clustername]";
public static final String DESC = "The delete-alias command removes the\n" +
"indicated alias from the --cluster specific\n" +
"credential store or the gateway credential store.";
private String name;
public AliasDeleteCommand(String alias) {
name = alias;
}
@Override
public void execute() throws Exception {
AliasService as = getAliasService();
KeystoreService keystoreService = getKeystoreService();
if (as != null) {
if (cluster == null) {
cluster = "__gateway";
}
boolean credentialStoreForClusterAvailable =
keystoreService.isCredentialStoreForClusterAvailable(cluster);
if (credentialStoreForClusterAvailable) {
List<String> aliasesForCluster = as.getAliasesForCluster(cluster);
if (null == aliasesForCluster || !aliasesForCluster.contains(name)) {
out.println("Deletion of Alias: " + name + " from cluster: " + cluster + " Failed. "
+ "\n" + "No such alias exists in the cluster.");
} else {
as.removeAliasForCluster(cluster, name);
out.println(name + " has been successfully deleted.");
}
} else {
out.println("Invalid cluster name provided: " + cluster);
}
}
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
public class MasterCreateCommand extends Command {
public static final String USAGE = "create-master [--force] [--master mastersecret] [--generate]";
public static final String DESC = "The create-master command persists the master secret in a file located at:\n" +
"{GATEWAY_HOME}/data/security/master.\n" +
"It will prompt the user for the secret to persist.\n" +
"Use --force to overwrite the master secret.\n" +
"Use --master to pass in a master secret to persist.\n" +
"This can be used to persist the secret without any user interaction.\n" +
"Be careful as the secret might appear in shell histories or process listings.\n" +
"Instead of --master it is usually a better idea to use --generate instead!\n" +
"Use --generate to have Knox automatically generate a random secret.\n" +
"The generated secret will not be printed or otherwise exposed.\n" +
"Do not specify both --master and --generate at the same time.\n";
public MasterCreateCommand() {
}
private GatewayConfig getGatewayConfig() {
GatewayConfig result;
Configuration conf = getConf();
if( conf instanceof GatewayConfig ) {
result = (GatewayConfig)conf;
} else {
result = new GatewayConfigImpl();
}
return result;
}
@Override
public boolean validate() {
boolean valid = true;
GatewayConfig config = getGatewayConfig();
File dir = new File( config.getGatewaySecurityDir() );
File file = new File( dir, "master" );
if( file.exists() ) {
if( force ) {
if( !file.canWrite() ) {
out.println(
"This command requires write permissions on the master secret file: " +
file.getAbsolutePath() );
valid = false;
} else {
valid = file.delete();
if( !valid ) {
out.println(
"Unable to delete the master secret file: " +
file.getAbsolutePath() );
}
}
} else {
out.println(
"Master secret is already present on disk. " +
"Please be aware that overwriting it will require updating other security artifacts. " +
" Use --force to overwrite the existing master secret." );
valid = false;
}
} else if( dir.exists() && !dir.canWrite() ) {
out.println(
"This command requires write permissions on the security directory: " +
dir.getAbsolutePath() );
valid = false;
}
return valid;
}
@Override
public void execute() throws Exception {
out.println("Master secret has been persisted to disk.");
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
private class VersionCommand extends Command {
public static final String USAGE = "version";
public static final String DESC = "Displays Knox version information.";
@Override
public void execute() throws Exception {
Properties buildProperties = loadBuildProperties();
System.out.println(
String.format(Locale.ROOT,
"Apache Knox: %s (%s)",
buildProperties.getProperty( "build.version", "unknown" ),
buildProperties.getProperty( "build.hash", "unknown" ) ) );
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
private class RedeployCommand extends Command {
public static final String USAGE = "redeploy [--cluster clustername]";
public static final String DESC =
"Redeploys one or all of the gateway's clusters (a.k.a topologies).";
@Override
public void execute() throws Exception {
TopologyService ts = getTopologyService();
ts.reloadTopologies();
if (cluster != null) {
if (validateClusterName(cluster, ts)) {
ts.redeployTopologies(cluster);
}
else {
out.println("Invalid cluster name provided. Nothing to redeploy.");
}
}
}
/**
* @param cluster Cluster name to validate against the TopologyService
* @param ts ToplogyService to validate the given cluster name
*/
private boolean validateClusterName(String cluster, TopologyService ts) {
boolean valid = false;
for (Topology t : ts.getTopologies() ) {
if (t.getName().equals(cluster)) {
valid = true;
break;
}
}
return valid;
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
private class ValidateTopologyCommand extends Command {
public static final String USAGE = "validate-topology [--cluster clustername] | [--path \"path/to/file\"]";
public static final String DESC = "Ensures that a cluster's description (a.k.a topology) \n" +
"follows the correct formatting rules.\n" +
"use the list-topologies command to get a list of available cluster names";
private String file = "";
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
@Override
public void execute() throws Exception {
GatewayConfig gc = getGatewayConfig();
String topDir = gc.getGatewayTopologyDir();
if(path != null) {
file = path;
} else if(cluster == null) {
// The following block of code retreieves the list of files in the topologies directory
File tops = new File(topDir + "/topologies");
if(tops.isDirectory()) {
out.println("List of files available in the topologies directory");
for (File f : tops.listFiles()) {
if(f.getName().endsWith(".xml")) {
String fName = f.getName().replace(".xml", "");
out.println(fName);
}
}
return;
} else {
out.println("Could not locate topologies directory");
return;
}
} else {
file = topDir + "/" + cluster + ".xml";
}
// The following block checks a topology against the XSD
out.println();
out.println("File to be validated: ");
out.println(file);
out.println("==========================================");
if(new File(file).exists()) {
TopologyValidator tv = new TopologyValidator(file);
if(tv.validateTopology()) {
out.println("Topology file validated successfully");
} else {
out.println(tv.getErrorString()) ;
out.println("Topology validation unsuccessful");
}
} else {
out.println("The topology file specified does not exist.");
}
}
}
private class ListTopologiesCommand extends Command {
public static final String USAGE = "list-topologies";
public static final String DESC = "Retrieves a list of the available topologies within the\n" +
"default topologies directory. Will return topologies that may not be deployed due\n" +
"errors in file formatting.";
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
@Override
public void execute() {
String confDir = getGatewayConfig().getGatewayConfDir();
File tops = new File(confDir + "/topologies");
out.println("List of files available in the topologies directory");
out.println(tops.toString());
if(tops.isDirectory()) {
for (File f : tops.listFiles()) {
if(f.getName().endsWith(".xml")) {
String fName = f.getName().replace(".xml", "");
out.println(fName);
}
}
return;
} else {
out.println("ERR: Topologies directory does not exist.");
return;
}
}
}
private class LDAPCommand extends Command {
public static final String USAGE = "ldap-command";
public static final String DESC = "This is an internal command. It should not be used.";
protected String username;
protected char[] password;
protected static final String debugMessage = "For more information use --d for debug output.";
protected Topology topology;
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
@Override
public void execute() {
out.println("This command does not have any functionality.");
}
// First define a few Exceptions
protected class NoSuchTopologyException extends Exception {
public NoSuchTopologyException() {}
public NoSuchTopologyException(String message) { super(message); }
}
protected class MissingPasswordException extends Exception {
public MissingPasswordException() {}
public MissingPasswordException(String message) { super(message); }
}
protected class MissingUsernameException extends Exception {
public MissingUsernameException() {}
public MissingUsernameException(String message) { super(message); }
}
protected class BadSubjectException extends Exception {
public BadSubjectException() {}
public BadSubjectException(String message) { super(message); }
}
protected class NoSuchProviderException extends Exception {
public NoSuchProviderException() {}
public NoSuchProviderException(String name, String role, String topology) {
super("Could not find provider with role: " + role + ", name: " + name + " inside of topology: " + topology);
}
}
// returns false if any errors are printed
protected boolean hasShiroProviderErrors(Topology topology, boolean groupLookup) {
// First let's define the variables that represent the ShiroProvider params
String mainLdapRealm = "main.ldapRealm";
String contextFactory = mainLdapRealm + ".contextFactory";
String groupContextFactory = "main.ldapGroupContextFactory";
String authorizationEnabled = mainLdapRealm + ".authorizationEnabled";
String userSearchAttributeName = mainLdapRealm + ".userSearchAttributeName";
String userObjectClass = mainLdapRealm + ".userObjectClass";
String searchBase = mainLdapRealm + ".searchBase";
String groupSearchBase = mainLdapRealm + ".groupSearchBase";
String userSearchBase = mainLdapRealm + ".userSearchBase";
String groupObjectClass = mainLdapRealm + ".groupObjectClass";
String memberAttribute = mainLdapRealm + ".memberAttribute";
String memberAttributeValueTemplate = mainLdapRealm + ".memberAttributeValueTemplate";
String systemUsername = contextFactory + ".systemUsername";
String systemPassword = contextFactory + ".systemPassword";
String url = contextFactory + ".url";
String userDnTemplate = mainLdapRealm + ".userDnTemplate";
Provider shiro = topology.getProvider("authentication", "ShiroProvider");
if(shiro != null) {
Map<String, String> params = shiro.getParams();
int errs = 0;
if(groupLookup) {
int errors = 0;
errors += hasParam(params, groupContextFactory, true) ? 0 : 1;
errors += hasParam(params, groupObjectClass, true) ? 0 : 1;
errors += hasParam(params, memberAttributeValueTemplate, true) ? 0 : 1;
errors += hasParam(params, memberAttribute, true) ? 0 : 1;
errors += hasParam(params, authorizationEnabled, true) ? 0 : 1;
errors += hasParam(params, systemUsername, true) ? 0 : 1;
errors += hasParam(params, systemPassword, true) ? 0 : 1;
errors += hasParam(params, userSearchBase, true) ? 0 : 1;
errors += hasParam(params, groupSearchBase, true) ? 0 : 1;
errs += errors;
} else {
// Realm + Url is always required.
errs += hasParam(params, mainLdapRealm, true) ? 0 : 1;
errs += hasParam(params, url, true) ? 0 : 1;
if(hasParam(params, authorizationEnabled, false)) {
int errors = 0;
int searchBaseErrors = 0;
errors += hasParam(params, systemUsername, true) ? 0 : 1;
errors += hasParam(params, systemPassword, true) ? 0 : 1;
searchBaseErrors += hasParam(params, searchBase, false) ? 0 : hasParam(params, userSearchBase, false) ? 0 : 1;
if (searchBaseErrors > 0) {
out.println("Warn: Both " + searchBase + " and " + userSearchBase + " are missing from the topology");
}
errors += searchBaseErrors;
errs += errors;
}
// If any one of these is present they must all be present
if( hasParam(params, userSearchAttributeName, false) ||
hasParam(params, userObjectClass, false) ||
hasParam(params, searchBase, false) ||
hasParam(params, userSearchBase, false)) {
int errors = 0;
errors += hasParam(params, userSearchAttributeName, true) ? 0 : 1;
errors += hasParam(params, userObjectClass, true) ? 0 : 1;
errors += hasParam(params, searchBase, false) ? 0 : hasParam(params, userSearchBase, false) ? 0 : 1;
errors += hasParam(params, systemUsername, true) ? 0 : 1;
errors += hasParam(params, systemPassword, true) ? 0 : 1;
if(errors > 0) {
out.println(userSearchAttributeName + " or " + userObjectClass + " or " + searchBase + " or " + userSearchBase + " was found in the topology");
out.println("If any one of the above params is present then " + userSearchAttributeName +
" and " + userObjectClass + " must both be present and either " + searchBase + " or " + userSearchBase + " must also be present.");
}
errs += errors;
} else {
errs += hasParam(params, userDnTemplate, true) ? 0 : 1;
}
}
return (errs > 0);
} else {
out.println("Could not obtain ShiroProvider");
return true;
}
}
// Checks to see if the param name is present. If not, notify the user
protected boolean hasParam(Map<String, String> params, String key, boolean notifyUser){
if(params.get(key) == null){
if(notifyUser) { out.println("Warn: " + key + " is not present in topology"); }
return false;
} else { return true; }
}
/**
*
* @param ini - the path to the shiro.ini file within a topology deployment.
* @param token - token for username and password
* @return - true/false whether a user was successfully able to authenticate or not.
*/
protected boolean authenticateUser(Ini ini, UsernamePasswordToken token){
boolean result = false;
try {
Subject subject = getSubject(ini);
try{
subject.login(token);
if(subject.isAuthenticated()){
result = true;
}
} catch (AuthenticationException e){
out.println(e.toString());
out.println(e.getCause().getMessage());
if (debug) {
e.printStackTrace(out);
} else {
out.println(debugMessage);
}
} finally {
subject.logout();
}
} catch (BadSubjectException e) {
out.println(e.toString());
if (debug){
e.printStackTrace();
} else {
out.println(debugMessage);
}
} catch (ConfigurationException e) {
out.println(e.toString());
} catch ( Exception e ) {
out.println(e.getCause());
out.println(e.toString());
}
return result;
}
protected boolean authenticateUser(String config, UsernamePasswordToken token) throws ConfigurationException {
Ini ini = new Ini();
ini.loadFromPath(config);
return authenticateUser(ini, token);
}
/**
*
* @param t - topology configuration to use
* @param config - the path to the shiro.ini file from the topology deployment.
* @return - true/false whether LDAP successfully authenticated with system credentials.
*/
protected boolean testSysBind(Topology t, String config) {
boolean result = false;
String username;
char[] password;
try {
// Pull out contextFactory.url param for light shiro config
Provider shiro = t.getProvider("authentication", "ShiroProvider");
Map<String, String> params = shiro.getParams();
String url = params.get("main.ldapRealm.contextFactory.url");
// Build the Ini with minimum requirements
Ini ini = new Ini();
ini.addSection("main");
ini.setSectionProperty("main", "ldapRealm", "org.apache.knox.gateway.shirorealm.KnoxLdapRealm");
ini.setSectionProperty("main", "ldapContextFactory", "org.apache.knox.gateway.shirorealm.KnoxLdapContextFactory");
ini.setSectionProperty("main", "ldapRealm.contextFactory.url", url);
username = getSystemUsername(t);
password = getSystemPassword(t);
result = authenticateUser(ini, new UsernamePasswordToken(username, password));
} catch (MissingUsernameException | NoSuchProviderException | MissingPasswordException | NullPointerException e) {
out.println(e.toString());
}
return result;
}
/**
*
* @param t - topology configuration to use
* @return - the principal of the systemUsername specified in topology. null if non-existent
*/
private String getSystemUsername(Topology t) throws MissingUsernameException, NoSuchProviderException {
final String SYSTEM_USERNAME = "main.ldapRealm.contextFactory.systemUsername";
String user;
Provider shiroProvider = t.getProvider("authentication", "ShiroProvider");
if(shiroProvider != null){
Map<String, String> params = shiroProvider.getParams();
user = params.get(SYSTEM_USERNAME);
} else {
throw new NoSuchProviderException("ShiroProvider", "authentication", t.getName());
}
return user;
}
/**
*
* @param t - topology configuration to use
* @return - the systemPassword specified in topology. null if non-existent
*/
private char[] getSystemPassword(Topology t) throws NoSuchProviderException, MissingPasswordException{
final String SYSTEM_PASSWORD = "main.ldapRealm.contextFactory.systemPassword";
String pass;
Provider shiro = t.getProvider("authentication", "ShiroProvider");
if(shiro != null){
Map<String, String> params = shiro.getParams();
pass = params.get(SYSTEM_PASSWORD);
} else {
throw new NoSuchProviderException("ShiroProvider", "authentication", t.getName());
}
if(pass != null) {
return pass.toCharArray();
} else {
throw new MissingPasswordException("ShiroProvider did not contain param: " + SYSTEM_PASSWORD);
}
}
/**
*
* @param config - the shiro.ini config file created in topology deployment.
* @return returns the Subject given by the shiro config's settings.
*/
protected Subject getSubject(Ini config) throws BadSubjectException {
try {
ThreadContext.unbindSubject();
@SuppressWarnings("deprecation")
Factory factory = new IniSecurityManagerFactory(config);
org.apache.shiro.mgt.SecurityManager securityManager = (org.apache.shiro.mgt.SecurityManager) factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
if( subject != null) {
return subject;
} else {
out.println("Error Creating Subject from config at: " + config);
}
} catch (Exception e){
out.println(e.toString());
}
throw new BadSubjectException("Subject could not be created with Shiro Config at " + config);
}
protected Subject getSubject(String config) throws ConfigurationException {
Ini ini = new Ini();
ini.loadFromPath(config);
try {
return getSubject(ini);
} catch (BadSubjectException e) {
throw new ConfigurationException("Could not get Subject with Ini at " + config);
}
}
/**
* prompts the user for credentials in the command line if necessary
* populates the username and password members.
*/
protected void promptCredentials() {
if(this.username == null){
Console c = System.console();
if( c != null) {
this.username = c.readLine("Username: ");
} else {
try(InputStreamReader inputStreamReader = new InputStreamReader(System.in, StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(inputStreamReader)) {
out.println("Username: ");
this.username = reader.readLine();
} catch (IOException e){
out.println(e.toString());
this.username = "";
}
}
}
if(this.password == null){
Console c = System.console();
if( c != null) {
this.password = c.readPassword("Password: ");
}else{
try(InputStreamReader inputStreamReader = new InputStreamReader(System.in, StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(inputStreamReader)) {
out.println("Password: ");
String pw = reader.readLine();
if(pw != null){
this.password = pw.toCharArray();
} else {
this.password = new char[0];
}
} catch (IOException e){
out.println(e.toString());
this.password = new char[0];
}
}
}
}
/**
*
* @param topologyName - the name of the topology to retrieve
* @return - Topology object with specified name. null if topology doesn't exist in TopologyService
*/
protected Topology getTopology(String topologyName) throws NoSuchTopologyException {
TopologyService ts = getTopologyService();
ts.reloadTopologies();
for (Topology t : ts.getTopologies()) {
if(t.getName().equals(topologyName)) {
return t;
}
}
throw new NoSuchTopologyException("Topology " + topologyName + " does not" +
" exist in the topologies directory.");
}
/**
*
* @param t - Topology to use for config
* @return - path of shiro.ini config file.
*/
protected String getConfig(Topology t){
File tmpDir = new File(System.getProperty("java.io.tmpdir"));
DeploymentFactory.setGatewayServices(services);
EnterpriseArchive archive = DeploymentFactory.createDeployment(getGatewayConfig(), t);
File war = archive.as(ExplodedExporter.class).exportExploded(tmpDir, t.getName() + "_deploy.tmp");
war.deleteOnExit();
String config = war.getAbsolutePath() + "/%2F/WEB-INF/shiro.ini";
try{
FileUtils.forceDeleteOnExit(war);
} catch (IOException e) {
out.println(e.toString());
war.deleteOnExit();
}
return config;
}
/**
* populates username and password if they were passed as arguments, if not will prompt user for them.
*/
void acquireCredentials(){
if(user != null){
this.username = user;
}
if(pass != null){
this.password = pass.toCharArray();
}
promptCredentials();
}
/**
*
* @return - true or false if the topology was acquired from the topology service and populated in the topology
* field.
*/
protected boolean acquireTopology(){
try {
topology = getTopology(cluster);
} catch (NoSuchTopologyException e) {
out.println(e.toString());
return false;
}
return true;
}
}
private class LDAPAuthCommand extends LDAPCommand {
public static final String USAGE = "user-auth-test [--cluster clustername] [--u username] [--p password] [--g]";
public static final String DESC = "This command tests a cluster's configuration ability to\n " +
"authenticate a user with a cluster's ShiroProvider settings.\n Use \"--g\" if you want to list the groups a" +
" user is a member of. \nOptional: [--u username]: Provide a username argument to the command\n" +
"Optional: [--p password]: Provide a password argument to the command.\n" +
"If a username and password argument are not supplied, the terminal will prompt you for one.";
private static final String SUBJECT_USER_GROUPS = "subject.userGroups";
private Set<String> groupSet = new HashSet<>();
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
@Override
public void execute() {
if(!acquireTopology()){
return;
}
acquireCredentials();
if(topology.getProvider("authentication", "ShiroProvider") == null) {
out.println("ERR: This tool currently only works with Shiro as the authentication provider.");
out.println("Please update the topology to use \"ShiroProvider\" as the authentication provider.");
return;
}
String config = getConfig(topology);
if(new File(config).exists()) {
if(authenticateUser(config, new UsernamePasswordToken(username, password))) {
out.println("LDAP authentication successful!");
if(groups) {
if(testSysBind(topology, config)) {
groupSet = getGroups(topology, new UsernamePasswordToken(username, password));
if(groupSet == null || groupSet.isEmpty()) {
out.println(username + " does not belong to any groups");
if(groups) {
hasShiroProviderErrors(topology, true);
out.println("You were looking for this user's groups but this user does not belong to any.");
out.println("Your topology file may be incorrectly configured for group lookup.");
}
} else {
for (Object o : groupSet.toArray()) {
out.println(username + " is a member of: " + o.toString());
}
}
}
}
} else {
out.println("ERR: Unable to authenticate user: " + username);
}
} else {
out.println("ERR: No shiro config file found.");
}
}
private Set<String> getGroups(Topology t, UsernamePasswordToken token){
Set<String> groups = null;
try {
Subject subject = getSubject(getConfig(t));
if(!subject.isAuthenticated()) {
subject.login(token);
}
subject.hasRole(""); //Populate subject groups
groups = (Set) subject.getSession().getAttribute(SUBJECT_USER_GROUPS);
subject.logout();
} catch (AuthenticationException e) {
out.println("Error retrieving groups");
out.println(e.toString());
if(debug) {
e.printStackTrace();
} else {
out.println(debugMessage);
}
} catch (ConfigurationException e) {
out.println(e.toString());
if(debug){
e.printStackTrace();
}
}
return groups;
}
}
public class LDAPSysBindCommand extends LDAPCommand {
public static final String USAGE = "system-user-auth-test [--cluster clustername] [--d]";
public static final String DESC = "This command tests a cluster configuration's ability to\n " +
"authenticate a user with a cluster's ShiroProvider settings.";
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
@Override
public void execute() {
if(!acquireTopology()) {
return;
}
if(hasShiroProviderErrors(topology, false)) {
out.println("Topology warnings present. SystemUser may not bind.");
}
if(testSysBind(topology, getConfig(topology))) {
out.println("System LDAP Bind successful.");
} else {
out.println("Unable to successfully bind to LDAP server with topology credentials. Are your parameters correct?");
}
}
}
private GatewayConfig getGatewayConfig() {
GatewayConfig result;
Configuration conf = getConf();
if(conf instanceof GatewayConfig) {
result = (GatewayConfig) conf;
} else {
result = new GatewayConfigImpl();
}
return result;
}
public class ServiceTestCommand extends Command {
public static final String USAGE = "service-test [--u username] [--p password] [--cluster clustername] [--hostname name] " +
"[--port port]";
public static final String DESC =
"This command requires a running instance of Knox to be present on the same machine.\n" +
"It will execute a test to make sure all services are accessible through the gateway URLs.\n" +
"Errors are reported and suggestions to resolve any problems are returned. JSON formatted.\n";
private boolean ssl = true;
private int attempts;
@Override
public String getUsage() { return USAGE + ":\n\n" + DESC; }
@Override
public void execute() {
attempts++;
SSLContext ctx = null;
CloseableHttpClient client;
String http = "http://";
String https = "https://";
GatewayConfig conf = getGatewayConfig();
String gatewayPort;
String host;
if(cluster == null) {
printKnoxShellUsage();
out.println("A --cluster argument is required.");
return;
}
if(hostname != null) {
host = hostname;
} else {
try {
host = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
out.println(e.toString());
out.println("Defaulting address to localhost. Use --hostname option to specify a different hostname");
host = "localhost";
}
}
if (port != null) {
gatewayPort = port;
} else if (conf.getGatewayPort() > -1) {
gatewayPort = Integer.toString(conf.getGatewayPort());
} else {
out.println("Could not get port. Please supply it using the --port option");
return;
}
String path = "/" + conf.getGatewayPath();
String topology = "/" + cluster;
String httpServiceTestURL = http + host + ":" + gatewayPort + path + topology + "/service-test";
String httpsServiceTestURL = https + host + ":" + gatewayPort + path + topology + "/service-test";
String authString = "";
// Create Authorization String
if( user != null && pass != null) {
authString = "Basic " + Base64.encodeBase64String((user + ":" + pass).getBytes(StandardCharsets.UTF_8));
} else {
out.println("Username and/or password not supplied. Expect HTTP 401 Unauthorized responses.");
}
// Attempt to build SSL context for HTTP client.
try {
ctx = SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
} catch (Exception e) {
out.println(e.toString());
}
// Initialize the HTTP client
if(ctx == null) {
client = HttpClients.createDefault();
} else {
client = HttpClients.custom().setSSLContext(ctx).build();
}
HttpGet request;
if(ssl) {
request = new HttpGet(httpsServiceTestURL);
} else {
request = new HttpGet(httpServiceTestURL);
}
request.setHeader("Authorization", authString);
request.setHeader("Accept", MediaType.APPLICATION_JSON.getMediaType());
try {
out.println(request.toString());
try(CloseableHttpResponse response = client.execute(request)) {
switch (response.getStatusLine().getStatusCode()) {
case 200:
response.getEntity().writeTo(out);
break;
case 404:
out.println("Could not find service-test resource");
out.println("Make sure you have configured the SERVICE-TEST service in your topology.");
break;
case 500:
out.println("HTTP 500 Server error");
break;
default:
out.println("Unexpected HTTP response code.");
out.println(response.getStatusLine().toString());
response.getEntity().writeTo(out);
break;
}
}
request.releaseConnection();
} catch (ClientProtocolException e) {
out.println(e.toString());
if (debug) {
e.printStackTrace(out);
}
} catch (SSLException e) {
out.println(e.toString());
retryRequest();
} catch (IOException e) {
out.println(e.toString());
retryRequest();
if(debug) {
e.printStackTrace(out);
}
} finally {
try {
client.close();
} catch (IOException e) {
out.println(e.toString());
}
}
}
public void retryRequest(){
if(attempts < 2) {
if(ssl) {
ssl = false;
out.println("Attempting request without SSL.");
} else {
ssl = true;
out.println("Attempting request with SSL ");
}
execute();
} else {
out.println("Unable to successfully make request. Try using the API with cURL.");
}
}
}
public class RemoteRegistryClientsListCommand extends Command {
static final String USAGE = "list-registry-clients";
static final String DESC = "Lists all of the remote configuration registry clients defined in gateway-site.xml.\n";
@Override
public void execute() throws Exception {
GatewayConfig config = getGatewayConfig();
List<String> remoteConfigRegistryClientNames = config.getRemoteRegistryConfigurationNames();
if (!remoteConfigRegistryClientNames.isEmpty()) {
out.println("Listing remote configuration registry clients:");
for (String name : remoteConfigRegistryClientNames) {
out.println(name);
}
}
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
private abstract class RemoteRegistryCommand extends Command {
static final String ROOT_ENTRY = "/knox";
static final String CONFIG_ENTRY = ROOT_ENTRY + "/config";
static final String PROVIDER_CONFIG_ENTRY = CONFIG_ENTRY + "/shared-providers";
static final String DESCRIPTORS_ENTRY = CONFIG_ENTRY + "/descriptors";
protected RemoteConfigurationRegistryClient getClient() {
RemoteConfigurationRegistryClient client = null;
if (remoteRegistryClient != null) {
RemoteConfigurationRegistryClientService cs = getRemoteConfigRegistryClientService();
client = cs.get(remoteRegistryClient);
if (client == null) {
out.println("No remote configuration registry identified by '" + remoteRegistryClient + "' could be found.");
}
} else {
out.println("Missing required argument : --registry-client\n");
}
return client;
}
}
public class RemoteRegistryListProviderConfigsCommand extends RemoteRegistryCommand {
static final String USAGE = "list-provider-configs --registry-client name";
static final String DESC = "Lists the provider configurations present in the specified remote registry\n";
@Override
public void execute() {
RemoteConfigurationRegistryClient client = getClient();
if (client != null) {
out.println("Provider Configurations (@" + client.getAddress() + ")");
List<String> entries = client.listChildEntries(PROVIDER_CONFIG_ENTRY);
if (entries != null) {
for (String entry : entries) {
out.println(entry);
}
}
out.println();
}
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
public class RemoteRegistryListDescriptorsCommand extends RemoteRegistryCommand {
static final String USAGE = "list-descriptors --registry-client name";
static final String DESC = "Lists the descriptors present in the specified remote registry\n";
@Override
public void execute() {
RemoteConfigurationRegistryClient client = getClient();
if (client != null) {
out.println("Descriptors (@" + client.getAddress() + ")");
List<String> entries = client.listChildEntries(DESCRIPTORS_ENTRY);
if (entries != null) {
for (String entry : entries) {
out.println(entry);
}
}
out.println();
}
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
/**
* Base class for remote config registry upload commands
*/
public abstract class RemoteRegistryUploadCommand extends RemoteRegistryCommand {
private File sourceFile;
protected String filename;
protected RemoteRegistryUploadCommand(String sourceFileName) {
this.filename = sourceFileName;
}
private void upload(RemoteConfigurationRegistryClient client, String entryPath, File source) throws Exception {
String content = FileUtils.readFileToString(source, StandardCharsets.UTF_8);
if (client.entryExists(entryPath)) {
// If it exists, then we're going to set the data
client.setEntryData(entryPath, content);
} else {
// If it does not exist, then create it and set the data
client.createEntry(entryPath, content);
}
}
File getSourceFile() {
if (sourceFile == null) {
sourceFile = new File(filename);
}
return sourceFile;
}
String getEntryName(String prefixPath) {
String entryName = remoteRegistryEntryName;
if (entryName == null) {
File sourceFile = getSourceFile();
if (sourceFile.exists()) {
String path = sourceFile.getAbsolutePath();
entryName = path.substring(path.lastIndexOf(File.separator) + 1);
} else {
out.println("Could not locate source file: " + filename);
}
}
return prefixPath + "/" + entryName;
}
protected void execute(String entryName, File sourceFile) throws Exception {
RemoteConfigurationRegistryClient client = getClient();
if (client != null) {
if (entryName != null) {
upload(client, entryName, sourceFile);
}
}
}
}
public class RemoteRegistryUploadProviderConfigCommand extends RemoteRegistryUploadCommand {
static final String USAGE = "upload-provider-config providerConfigFile --registry-client name [--entry-name entryName]";
static final String DESC = "Uploads a provider configuration to the specified remote registry client, optionally " +
"renaming the entry.\nIf the entry name is not specified, the name of the uploaded " +
"file is used.\n";
RemoteRegistryUploadProviderConfigCommand(String fileName) {
super(fileName);
}
@Override
public void execute() throws Exception {
super.execute(getEntryName(PROVIDER_CONFIG_ENTRY), getSourceFile());
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
public class RemoteRegistryUploadDescriptorCommand extends RemoteRegistryUploadCommand {
static final String USAGE = "upload-descriptor descriptorFile --registry-client name [--entry-name entryName]";
static final String DESC = "Uploads a simple descriptor using the specified remote registry client, optionally " +
"renaming the entry.\nIf the entry name is not specified, the name of the uploaded " +
"file is used.\n";
RemoteRegistryUploadDescriptorCommand(String fileName) {
super(fileName);
}
@Override
public void execute() throws Exception {
super.execute(getEntryName(DESCRIPTORS_ENTRY), getSourceFile());
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
public class RemoteRegistryGetACLCommand extends RemoteRegistryCommand {
static final String USAGE = "get-registry-acl entry --registry-client name";
static final String DESC = "Presents the ACL settings for the specified remote registry entry.\n";
private String entry;
RemoteRegistryGetACLCommand(String entry) {
this.entry = entry;
}
@Override
public void execute() throws Exception {
RemoteConfigurationRegistryClient client = getClient();
if (client != null) {
if (entry != null) {
List<RemoteConfigurationRegistryClient.EntryACL> acls = client.getACL(entry);
for (RemoteConfigurationRegistryClient.EntryACL acl : acls) {
out.println(acl.getType() + ":" + acl.getId() + ":" + acl.getPermissions());
}
}
}
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
/**
* Base class for remote config registry delete commands
*/
public abstract class RemoteRegistryDeleteCommand extends RemoteRegistryCommand {
protected String entryName;
protected RemoteRegistryDeleteCommand(String entryName) {
this.entryName = entryName;
}
private void delete(RemoteConfigurationRegistryClient client, String entryPath) throws Exception {
if (client.entryExists(entryPath)) {
// If it exists, then delete it
client.deleteEntry(entryPath);
}
}
protected void execute(String entryName) throws Exception {
RemoteConfigurationRegistryClient client = getClient();
if (client != null) {
if (entryName != null) {
delete(client, entryName);
}
}
}
}
public class RemoteRegistryDeleteProviderConfigCommand extends RemoteRegistryDeleteCommand {
static final String USAGE = "delete-provider-config providerConfig --registry-client name";
static final String DESC = "Deletes a shared provider configuration from the specified remote registry.\n";
public RemoteRegistryDeleteProviderConfigCommand(String entryName) {
super(entryName);
}
@Override
public void execute() throws Exception {
execute(PROVIDER_CONFIG_ENTRY + "/" + entryName);
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
public class RemoteRegistryDeleteDescriptorCommand extends RemoteRegistryDeleteCommand {
static final String USAGE = "delete-descriptor descriptor --registry-client name";
static final String DESC = "Deletes a simple descriptor from the specified remote registry.\n";
public RemoteRegistryDeleteDescriptorCommand(String entryName) {
super(entryName);
}
@Override
public void execute() throws Exception {
execute(DESCRIPTORS_ENTRY + "/" + entryName);
}
@Override
public String getUsage() {
return USAGE + ":\n\n" + DESC;
}
}
private static Properties loadBuildProperties() {
Properties properties = new Properties();
try(InputStream inputStream = KnoxCLI.class.getClassLoader().getResourceAsStream( "build.properties" )) {
properties.load(inputStream);
} catch( IOException e ) {
// Ignore.
}
return properties;
}
public static void main(String[] args) throws Exception {
PropertyConfigurator.configure( System.getProperty( "log4j.configuration" ) );
int res = ToolRunner.run(new GatewayConfigImpl(), new KnoxCLI(), args);
System.exit(res);
}
}