Java tutorial
/* * JBoss, Home of Professional Open Source * * Copyright 2018 Red Hat, Inc. and/or its affiliates. * * 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 org.wildfly.security.tool; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.security.Security; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.lang3.ArrayUtils; import org.wildfly.common.iteration.ByteIterator; import org.wildfly.security.WildFlyElytronProvider; import org.wildfly.security.auth.principal.NamePrincipal; import org.wildfly.security.auth.realm.FileSystemSecurityRealm; import org.wildfly.security.auth.server.ModifiableRealmIdentity; import org.wildfly.security.authz.MapAttributes; import org.wildfly.security.credential.PasswordCredential; import org.wildfly.security.password.PasswordFactory; import org.wildfly.security.password.interfaces.DigestPassword; import org.wildfly.security.password.spec.DigestPasswordSpec; import org.wildfly.security.password.spec.PasswordSpec; /** * Elytron-Tool command to convert legacy properties file into a FileSystemRealm. * Also, optionally provides a WildFly CLI script to register the FileSystemRealm and corresponding * security-domain in WildFly. * @author <a href="mailto:jucook@redhat.com">Justin Cook</a> */ class FileSystemRealmCommand extends Command { static final int GENERAL_CONFIGURATION_WARNING = 1; static final String FILE_SYSTEM_REALM_COMMAND = "filesystem-realm"; static final int SUMMARY_WIDTH = 100; private static final String HELP_PARAM = "help"; private static final String DEBUG_PARAM = "debug"; private static final String SILENT_PARAM = "silent"; private static final String SUMMARY_PARAM = "summary"; private static final String USERS_FILE_PARAM = "users-file"; private static final String ROLES_FILE_PARAM = "roles-file"; private static final String OUTPUT_LOCATION_PARAM = "output-location"; private static final String FILESYSTEM_REALM_NAME_PARAM = "filesystem-realm-name"; private static final String SECURITY_DOMAIN_NAME_PARAM = "security-domain-name"; private static final String BULK_CONVERT_PARAM = "bulk-convert"; private static final String FILE_ARG = "file"; private static final String DIRECTORY_ARG = "directory"; private static final String NAME_ARG = "name"; private static final String DEFAULT_FILESYSTEM_REALM_NAME = "converted-properties-filesystem-realm"; private static final String DEFAULT_SECURITY_DOMAIN_NAME = "converted-properties-security-domain"; private List<Descriptor> descriptors = new ArrayList<>(); private final List<String> PARAMS_LIST = new ArrayList<>(Arrays.asList(USERS_FILE_PARAM, ROLES_FILE_PARAM, OUTPUT_LOCATION_PARAM, FILESYSTEM_REALM_NAME_PARAM, SECURITY_DOMAIN_NAME_PARAM)); private Options options; private CommandLineParser parser = new DefaultParser(); private CommandLine cmdLine = null; private boolean silentMode = false; private boolean summaryMode = false; private StringBuilder summaryString = null; private boolean warningOccurred = false; FileSystemRealmCommand() { options = new Options(); Option option; option = new Option("u", USERS_FILE_PARAM, true, ElytronToolMessages.msg.cmdFileSystemRealmUsersFileDesc()); option.setArgName(FILE_ARG); options.addOption(option); option = new Option("r", ROLES_FILE_PARAM, true, ElytronToolMessages.msg.cmdFileSystemRealmRolesFileDesc()); option.setArgName(FILE_ARG); options.addOption(option); option = new Option("o", OUTPUT_LOCATION_PARAM, true, ElytronToolMessages.msg.cmdFileSystemRealmOutputLocationDesc()); option.setArgName(DIRECTORY_ARG); options.addOption(option); option = new Option("b", BULK_CONVERT_PARAM, true, ElytronToolMessages.msg.cmdFileSystemRealmBulkConvertDesc()); option.setArgName(NAME_ARG); options.addOption(option); option = new Option("f", FILESYSTEM_REALM_NAME_PARAM, true, ElytronToolMessages.msg.cmdFileSystemRealmFileSystemRealmNameDesc()); option.setArgName(NAME_ARG); options.addOption(option); option = new Option("s", SECURITY_DOMAIN_NAME_PARAM, true, ElytronToolMessages.msg.cmdFileSystemRealmSecurityDomainNameDesc()); option.setArgName(NAME_ARG); options.addOption(option); option = Option.builder().longOpt(HELP_PARAM).desc(ElytronToolMessages.msg.cmdLineHelp()).build(); options.addOption(option); option = Option.builder().longOpt(DEBUG_PARAM).desc(ElytronToolMessages.msg.cmdLineDebug()).build(); options.addOption(option); option = Option.builder().longOpt(SILENT_PARAM).desc(ElytronToolMessages.msg.cmdFileSystemRealmSilentDesc()) .build(); options.addOption(option); option = Option.builder().longOpt(SUMMARY_PARAM) .desc(ElytronToolMessages.msg.cmdFileSystemRealmSummaryDesc()).build(); options.addOption(option); } private static final class Descriptor { private String usersFile; private String rolesFile; private String outputLocation; private String fileSystemRealmName; private String securityDomainName; private String realmName; Descriptor() { } Descriptor(Descriptor descriptor) { this.usersFile = descriptor.usersFile; this.rolesFile = descriptor.rolesFile; this.outputLocation = descriptor.outputLocation; this.fileSystemRealmName = descriptor.fileSystemRealmName; this.securityDomainName = descriptor.securityDomainName; this.realmName = descriptor.realmName; } String getUsersFile() { return this.usersFile; } String getRolesFile() { return this.rolesFile; } String getOutputLocation() { return this.outputLocation; } String getFileSystemRealmName() { return this.fileSystemRealmName; } String getSecurityDomainName() { return this.securityDomainName; } String getRealmName() { return this.realmName; } void setUsersFile(String usersFile) { this.usersFile = usersFile; } void setRolesFile(String rolesFile) { this.rolesFile = rolesFile; } void setOutputLocation(String outputLocation) { this.outputLocation = outputLocation; } void setFileSystemRealmName(String fileSystemRealmName) { this.fileSystemRealmName = fileSystemRealmName; } void setSecurityDomainName(String securityDomainName) { this.securityDomainName = securityDomainName; } void setRealmName(String realmName) { this.realmName = realmName; } void reset() { this.usersFile = null; this.rolesFile = null; this.outputLocation = null; this.fileSystemRealmName = null; this.securityDomainName = null; this.realmName = null; } } @Override public void execute(String[] args) throws Exception { setStatus(GENERAL_CONFIGURATION_ERROR); cmdLine = parser.parse(options, args, false); setEnableDebug(cmdLine.hasOption(DEBUG_PARAM)); if (cmdLine.hasOption(HELP_PARAM)) { help(); setStatus(ElytronTool.ElytronToolExitStatus_OK); return; } if (cmdLine.hasOption(SILENT_PARAM)) { silentMode = true; } if (cmdLine.hasOption(SUMMARY_PARAM)) { summaryMode = true; summaryString = new StringBuilder(); summaryString.append(String.join("", Collections.nCopies(SUMMARY_WIDTH, "-"))); summaryString.append(System.getProperty("line.separator")); summaryString.append("Summary for execution of Elytron-Tool command FileSystemRealm"); summaryString.append(System.getProperty("line.separator")); summaryString.append(String.join("", Collections.nCopies(SUMMARY_WIDTH, "-"))); summaryString.append(System.getProperty("line.separator")); } printDuplicatesWarning(cmdLine); String bulkConvert = cmdLine.getOptionValue("b"); String usersFileOption = cmdLine.getOptionValue("u"); String rolesFileOption = cmdLine.getOptionValue("r"); String outputLocationOption = cmdLine.getOptionValue("o"); if (bulkConvert == null) { if (summaryMode) { summaryString.append("Options were specified via CLI, converting single users-roles combination"); summaryString.append(System.getProperty("line.separator")); } if (usersFileOption == null) { errorHandler(ElytronToolMessages.msg.missingUsersFile()); } else if (rolesFileOption == null) { errorHandler(ElytronToolMessages.msg.missingRolesFile()); } else if (outputLocationOption == null) { errorHandler(ElytronToolMessages.msg.missingOutputLocation()); } Descriptor descriptor = new Descriptor(); descriptor.setUsersFile(usersFileOption); descriptor.setRolesFile(rolesFileOption); descriptor.setOutputLocation(outputLocationOption); descriptor.setFileSystemRealmName(cmdLine.getOptionValue("f")); descriptor.setSecurityDomainName(cmdLine.getOptionValue("s")); descriptors.add(descriptor); findMissingRequiredValuesAndSetValues(0, descriptor); } else if (usersFileOption != null || rolesFileOption != null || outputLocationOption != null) { throw ElytronToolMessages.msg.mutuallyExclusiveOptionsSpecified(); } else { if (summaryMode) { summaryString.append(String.format( "Options were specified via descriptor file: %s, converting multiple users-roles combinations", bulkConvert)); summaryString.append(System.getProperty("line.separator")); } parseDescriptorFile(bulkConvert); } createFileSystemRealm(); createWildFlyScript(); if (summaryMode) { summaryString.append(String.join("", Collections.nCopies(SUMMARY_WIDTH, "-"))); summaryString.append(System.getProperty("line.separator")); summaryString.append("End of summary"); summaryString.append(System.getProperty("line.separator")); summaryString.append(String.join("", Collections.nCopies(SUMMARY_WIDTH, "-"))); System.out.println(summaryString); } if (warningOccurred) { setStatus(GENERAL_CONFIGURATION_WARNING); } else { setStatus(ElytronTool.ElytronToolExitStatus_OK); } } /** * Displays the help for the command */ @Override public void help() { HelpFormatter help = new HelpFormatter(); help.setWidth(WIDTH); help.printHelp(ElytronToolMessages.msg.cmdHelp(getToolCommand(), FILE_SYSTEM_REALM_COMMAND), ElytronToolMessages.msg.cmdFileSystemRealmHelpHeader(), options, "", true); } /** * Prints out a warning message if silentMode is not enabled and adds the warning to the summary * if summaryMode is enabled * * @param warning The warning to be shown */ private void warningHandler(String warning) { warningOccurred = true; if (!silentMode) { System.out.print("WARNING: "); System.out.println(warning); } if (summaryMode) { summaryString.append("WARNING: "); summaryString.append(warning); summaryString.append(System.getProperty("line.separator")); } } /** * Determines if a summary needs to be printed and prints summary after an error is thrown * * @param e The exception thrown during execution * @throws Exception The exception to be handled by Elytron Tool */ private void errorHandler(Exception e) throws Exception { setStatus(GENERAL_CONFIGURATION_ERROR); if (summaryMode) { summaryString.append("Error was thrown during execution:"); summaryString.append(System.getProperty("line.separator")); summaryString.append(e.getMessage()); System.out.println(System.getProperty("line.separator") + summaryString.toString()); } throw e; } /** * Prints out information found in a descriptor file for summary mode * * @param count The amount of descriptor blocks in the file */ private void printDescriptorBlocks(int count) { summaryString.append(System.getProperty("line.separator")); summaryString.append(System.getProperty("line.separator")); summaryString .append("Found following users-roles combinations, null indicates missing required component:"); summaryString.append(System.getProperty("line.separator")); for (int i = 0; i < count; i++) { StringBuilder summary = new StringBuilder(); summary.append("\tPrinting summary for block "); summary.append(i + 1); summary.append(System.getProperty("line.separator")); Descriptor descriptor = descriptors.get(i); for (String param : PARAMS_LIST) { summary.append("\t\t"); summary.append(param); summary.append(" - "); summary.append(getDescriptorParam(param, descriptor)); summary.append(System.getProperty("line.separator")); } summaryString.append(summary); } summaryString.append(System.getProperty("line.separator")); } /** * Returns the value for a given param in a Descriptor object * * @param param The parameter to be obtained from the Descriptor object * @param descriptor The Descriptor object to be used * @return The value of the given parameter */ private String getDescriptorParam(String param, Descriptor descriptor) { switch (param) { case USERS_FILE_PARAM: return descriptor.getUsersFile(); case ROLES_FILE_PARAM: return descriptor.getRolesFile(); case OUTPUT_LOCATION_PARAM: return descriptor.getOutputLocation(); case FILESYSTEM_REALM_NAME_PARAM: return descriptor.getFileSystemRealmName(); case SECURITY_DOMAIN_NAME_PARAM: return descriptor.getSecurityDomainName(); default: return null; } } /** * Handles input being given as a descriptor file * * @throws Exception Exception to be handled by Elytron Tool */ private void parseDescriptorFile(String file) throws Exception { Path path = Paths.get(file); if (!path.toFile().exists()) { errorHandler(ElytronToolMessages.msg.fileNotFound(file)); } Descriptor descriptor = new Descriptor(); AtomicInteger count = new AtomicInteger(1); try (Stream<String> stream = Files.lines(path)) { stream.forEach(line -> { if (line.equals("")) { findMissingRequiredValuesAndSetValues(count.intValue(), descriptor); copyAddResetDescriptor(descriptor); count.getAndIncrement(); } else { // Since Windows URIs have a colon, only split at first occurrence String[] parts = line.split(":", 2); String option = parts[0]; String arg = parts[1]; switch (option) { case USERS_FILE_PARAM: descriptor.setUsersFile(arg); break; case ROLES_FILE_PARAM: descriptor.setRolesFile(arg); break; case OUTPUT_LOCATION_PARAM: descriptor.setOutputLocation(arg); break; case FILESYSTEM_REALM_NAME_PARAM: descriptor.setFileSystemRealmName(arg); break; case SECURITY_DOMAIN_NAME_PARAM: descriptor.setSecurityDomainName(arg); break; } } }); } catch (IOException e) { errorHandler(e); } int currentCount = count.intValue(); findMissingRequiredValuesAndSetValues(currentCount, descriptor); copyAddResetDescriptor(descriptor); if (summaryMode) { printDescriptorBlocks(currentCount); } count.getAndIncrement(); } /** * Copies a Descriptor into a temporary one for it can be cloned into the list of descriptors * * @param original The original descriptor that is continually modified */ private void copyAddResetDescriptor(Descriptor original) { Descriptor temp = new Descriptor(original); descriptors.add(temp); original.reset(); } /** * Determines if the current descriptor block is missing any required values * and then if it is, prints out a warning message and sets that required value * and the optional values to null * * @param count The index of the current descriptor block */ private void findMissingRequiredValuesAndSetValues(int count, Descriptor descriptor) { boolean missingRequiredValue = false; if (descriptor.getUsersFile() == null) { warningHandler(ElytronToolMessages.msg.skippingDescriptorBlock(count, "missing users-file")); missingRequiredValue = true; } if (descriptor.getRolesFile() == null) { warningHandler(ElytronToolMessages.msg.skippingDescriptorBlock(count, "missing roles-file")); missingRequiredValue = true; } if (descriptor.getOutputLocation() == null) { warningHandler(ElytronToolMessages.msg.skippingDescriptorBlock(count, "missing output-location")); missingRequiredValue = true; } if (missingRequiredValue) { descriptor.reset(); } } /** * Parses a uses or roles properties file to get the user mappings (users-password for users files * and users-roles for roles files). * * @param descriptor The descriptor class holding the information for this block * @param param The type of input file (either users-file or roles-file) * @param count The index of the current descriptor block * @return The String list of each user mapping in the file * @throws Exception Exception to be handled by Elytron Tool */ private List<String> parseInputFile(Descriptor descriptor, String param, int count) throws Exception { List<String> entries = new ArrayList<>(); String paramValue = getDescriptorParam(param, descriptor); boolean valueValid = true; Path path = null; if (paramValue == null) { warningHandler(ElytronToolMessages.msg.noValueFound(param)); valueValid = false; } else { path = Paths.get(paramValue); if (!path.toFile().exists()) { if (descriptors.size() == 1) errorHandler(ElytronToolMessages.msg.fileNotFound(paramValue)); warningHandler(ElytronToolMessages.msg.fileNotFound(paramValue).getMessage()); warningHandler(ElytronToolMessages.msg.skippingDescriptorBlock(count, String.format("could not find file for %s", param))); valueValid = false; } } if (valueValid) { try (Stream<String> stream = Files.lines(path)) { stream.forEach(line -> { if (line.startsWith("#$REALM_NAME=")) { line = line.substring(line.indexOf("=") + 1); line = line.substring(0, line.indexOf("$")); descriptor.setRealmName(line); } else if (!line.startsWith("#")) { entries.add(line); } }); } } return entries; } /** * Handles creating the Elytron filesystem-realm from the input arrays * * @throws Exception Exception to be handled by Elytron Tool */ private void createFileSystemRealm() throws Exception { Security.addProvider(new WildFlyElytronProvider()); for (int i = 0; i < descriptors.size(); i++) { Descriptor descriptor = descriptors.get(i); if (descriptor.getUsersFile() == null || descriptor.getRolesFile() == null || descriptor.getOutputLocation() == null) { continue; } List<String> usersList = parseInputFile(descriptor, USERS_FILE_PARAM, i + 1); List<String> rolesList = parseInputFile(descriptor, ROLES_FILE_PARAM, i + 1); if (usersList.isEmpty() || rolesList.isEmpty()) { descriptor.reset(); continue; } FileSystemSecurityRealm newFileSystemRealm = new FileSystemSecurityRealm( Paths.get(descriptor.getOutputLocation())); Map<String, ArrayList<String>> usersMap = new HashMap<>(); for (String userMapping : usersList) { String[] userStringSplit = userMapping.split("="); String user = userStringSplit[0].trim(); String password; if (userStringSplit.length == 1) { String message = String.format("No password was found for user %s", user); warningHandler(message); password = null; } else { password = userStringSplit[1].trim(); } ArrayList<String> userAttributes = new ArrayList<>(); userAttributes.add(password); usersMap.put(user, userAttributes); } for (String rolesMapping : rolesList) { String[] rolesStringSplit = rolesMapping.split("="); String user = rolesStringSplit[0].trim(); String[] roles = new String[] {}; if (rolesStringSplit.length < 2) { String message = String.format("No roles were found for user %s", user); warningHandler(message); } else { roles = rolesStringSplit[1].trim().split(","); } ArrayList<String> userAttributes = usersMap.get(user); if (userAttributes == null) { String message = String.format("Roles were found for user %1$s, but user %1$s was not defined.", user); warningHandler(message); ArrayList<String> attributesWithEmptyPassword = new ArrayList<>(); attributesWithEmptyPassword.add(null); attributesWithEmptyPassword.addAll(new ArrayList<>(Arrays.asList(roles))); userAttributes = attributesWithEmptyPassword; usersMap.put(user, userAttributes); } else { userAttributes.addAll(Arrays.asList(roles)); usersMap.replace(user, userAttributes); } if (summaryMode) { summaryString.append( String.format("Added roles: %s for user %s.", ArrayUtils.toString(roles), user)); summaryString.append(System.getProperty("line.separator")); } } usersMap.forEach((key, value) -> { ModifiableRealmIdentity identity = newFileSystemRealm .getRealmIdentityForUpdate(new NamePrincipal(key)); try { identity.create(); MapAttributes attributes = new MapAttributes(); attributes.addAll("roles", value.subList(1, value.size())); identity.setAttributes(attributes); String password = value.get(0); if (password != null) { byte[] hashed = ByteIterator.ofBytes(password.getBytes(StandardCharsets.UTF_8)) .asUtf8String().hexDecode().drain(); PasswordSpec passwordSpec = new DigestPasswordSpec(key, descriptor.getRealmName(), hashed); PasswordFactory factory = PasswordFactory.getInstance(DigestPassword.ALGORITHM_DIGEST_MD5); DigestPassword digestPassword = (DigestPassword) factory.generatePassword(passwordSpec); identity.setCredentials(Collections.singleton(new PasswordCredential(digestPassword))); } identity.dispose(); } catch (NullPointerException e) { warningHandler(String.format("Could not read realm name from the users file")); } catch (Exception e) { warningHandler(String.format("Could not create realm for user %s due to error: ", key) + e.getMessage()); } }); } } /** * Creates the script/commands the user must run for Elytron to recognize * and use the new filesystem-realm */ private void createWildFlyScript() throws Exception { for (Descriptor descriptor : descriptors) { String usersFile = descriptor.getUsersFile(); if (descriptor.getUsersFile() == null || descriptor.getRolesFile() == null || descriptor.getOutputLocation() == null) { continue; } String fileSystemRealmName = descriptor.getFileSystemRealmName(); if (fileSystemRealmName == null || fileSystemRealmName.isEmpty()) { warningHandler(String.format( "No name provided for filesystem-realm, using default filesystem-realm name for %s.", usersFile)); descriptor.setFileSystemRealmName(DEFAULT_FILESYSTEM_REALM_NAME); fileSystemRealmName = DEFAULT_FILESYSTEM_REALM_NAME; } String outputLocation = descriptor.getOutputLocation(); String securityDomainName = descriptor.getSecurityDomainName(); String createScriptCheck = ""; if (Paths.get(String.format("%s.sh", fileSystemRealmName)).toFile().exists()) { createScriptCheck = prompt(false, null, false, ElytronToolMessages.msg .shouldFileBeOverwritten(String.format("%s.sh", fileSystemRealmName))); } String fullOutputPath; if (outputLocation.startsWith(".")) { fullOutputPath = Paths.get(outputLocation.substring(2, outputLocation.length())).toAbsolutePath() .toString(); } else { fullOutputPath = Paths.get(outputLocation).toAbsolutePath().toString(); } if (summaryMode) { summaryString.append(String.format("Configured script for WildFly named %s.sh at %s.", fileSystemRealmName, fullOutputPath)); summaryString.append(System.getProperty("line.separator")); summaryString.append("The script is using the following names:"); summaryString.append(System.getProperty("line.separator")); summaryString.append(String.format("Name of filesystem-realm: %s", fileSystemRealmName)); summaryString.append(System.getProperty("line.separator")); } if (securityDomainName != null && !securityDomainName.isEmpty()) { if (summaryMode) { summaryString.append(String.format("Name of security-domain: %s", securityDomainName)); summaryString.append(System.getProperty("line.separator")); } } else { warningHandler(String.format( "No name provided for security-domain, using default security-domain name for %s.", usersFile)); securityDomainName = DEFAULT_SECURITY_DOMAIN_NAME; } List<String> scriptLines = Arrays.asList( String.format("/subsystem=elytron/filesystem-realm=%s:add(path=%s)", fileSystemRealmName, fullOutputPath), String.format( "/subsystem=elytron/security-domain=%1$s:add(realms=[{realm=%2$s}],default-realm=%2$s,permission-mapper=default-permission-mapper)", securityDomainName, fileSystemRealmName)); if (!createScriptCheck.equals("y") && !createScriptCheck.equals("yes")) { Files.write(Paths.get(String.format("%s/%s.sh", outputLocation, fileSystemRealmName)), scriptLines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } else { Files.write(Paths.get(String.format("%s/%s.sh", outputLocation, fileSystemRealmName)), scriptLines, StandardOpenOption.APPEND); } } } }