diff --git a/domain-management/src/main/java/org/jboss/as/domain/management/logging/DomainManagementLogger.java b/domain-management/src/main/java/org/jboss/as/domain/management/logging/DomainManagementLogger.java index bf0fe37bc30..edf879c5770 100644 --- a/domain-management/src/main/java/org/jboss/as/domain/management/logging/DomainManagementLogger.java +++ b/domain-management/src/main/java/org/jboss/as/domain/management/logging/DomainManagementLogger.java @@ -1241,6 +1241,15 @@ public interface DomainManagementLogger extends BasicLogger { @Message(id = 146, value = "Outbound connections are no longer supported, please remove them from the configuration.") XMLStreamException outboundConnectionsUnsupported(); + /** + * Message stating that the required file or directory structure could not be created. + * + * @param path the path that could not be created. + * @return the message. + */ + @Message(id = 147, value = "Failed to create the required configuration path: %s") + String failedToCreateFilesConfigurationPath(String path); + /** * Information message saying the username and password must be different. * diff --git a/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/AddUser.java b/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/AddUser.java index 0a678e9b06c..993b3697a40 100644 --- a/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/AddUser.java +++ b/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/AddUser.java @@ -139,6 +139,8 @@ public static void main(String[] args) { } if (CommandLineArgument.DOMAIN_CONFIG_DIR_USERS.match(temp)) { options.setDomainConfigDir(it.next()); + } else if (CommandLineArgument.CREATE_FILES.match(temp)) { + options.setCreateFiles(true); } else if (CommandLineArgument.SERVER_CONFIG_DIR_USERS.match(temp)) { options.setServerConfigDir(it.next()); } else if (CommandLineArgument.APPLICATION_USERS.match(temp)) { @@ -266,6 +268,12 @@ public String instructions() { return DomainManagementLogger.ROOT_LOGGER.argDomainConfigDirUsers(); } }, + CREATE_FILES("-cf", "--create-files") { + @Override + public String instructions() { + return "Create files & directories if they do not exist"; + } + }, SERVER_CONFIG_DIR_USERS("-sc") { @Override public String argumentExample() { diff --git a/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/PropertyFileFinder.java b/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/PropertyFileFinder.java index 19f4b394e3c..b965a82c5e8 100644 --- a/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/PropertyFileFinder.java +++ b/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/PropertyFileFinder.java @@ -18,6 +18,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -42,6 +43,8 @@ public class PropertyFileFinder implements State { private final StateValues stateValues; private boolean validFilePermissions = true; private String filePermissionsProblemPath; + private boolean createFilesFailed = false; + private String createFilesFailedCause; public PropertyFileFinder(ConsoleWrapper theConsole, final StateValues stateValues) { this.theConsole = theConsole; @@ -61,6 +64,9 @@ public State execute() { fileName = fileName == null ? stateValues.getFileMode() == FileMode.MANAGEMENT ? MGMT_USERS_PROPERTIES : APPLICATION_USERS_PROPERTIES : fileName; if (!findFiles(foundFiles, fileName)) { + if (createFilesFailed) { + return new ErrorState(theConsole, DomainManagementLogger.ROOT_LOGGER.failedToCreateFilesConfigurationPath(createFilesFailedCause), null, null); + } return new ErrorState(theConsole, DomainManagementLogger.ROOT_LOGGER.propertiesFileNotFound(fileName), null, stateValues); } else if(!validFilePermissions) { return new ErrorState(theConsole, DomainManagementLogger.ROOT_LOGGER.filePermissionsProblemsFound( @@ -180,6 +186,7 @@ private boolean findFiles(final List foundFiles, final String fileName) { String serverConfigOpt = stateValues.getOptions().getServerConfigDir(); String domainConfigOpt = stateValues.getOptions().getDomainConfigDir(); + Boolean createFilesOpt = stateValues.getOptions().isCreateFiles(); // if no option is set, add to both standalone and domain if (serverConfigOpt == null && domainConfigOpt == null) { @@ -201,7 +208,10 @@ private boolean findFiles(final List foundFiles, final String fileName) { SERVER_CONFIG_DIR, SERVER_BASE_DIR, "standalone", fileName); if (standaloneProps.exists()) { foundFiles.add(standaloneProps); - } // TODO should this invalid --sc be an error regardless of whether domainConfigOpt points to a valid dir? + } else if (createFilesOpt) { + createFiles(standaloneProps, foundFiles); + } + // TODO should this invalid --sc be an error regardless of whether domainConfigOpt points to a valid dir? } if (domainConfigOpt != null) { @@ -209,7 +219,10 @@ private boolean findFiles(final List foundFiles, final String fileName) { DOMAIN_CONFIG_DIR, DOMAIN_BASE_DIR, "domain", fileName); if (domainProps.exists()) { foundFiles.add(domainProps); - } // TODO should this invalid --dc be an error regardless of whether serverConfigOpt points to a valid dir? + } else if (createFilesOpt) { + createFiles(domainProps, foundFiles); + } + // TODO should this invalid --dc be an error regardless of whether serverConfigOpt points to a valid dir? } return !foundFiles.isEmpty(); @@ -290,4 +303,31 @@ private void validatePermissions(final File dirPath, final File file) { } + /** + * Attempts to create the configuration file and its parent directories if they do not exist. + * This is used when the --create-files (-cf) flag is provided to ensure the environment + * is ready for user creation, especially in cloud or containerized deployments. + * If an error occurs, the {@code createFilesFailed} flag is set to true and the + * cause is captured in {@code createFilesFailedCause}. + * + * @param file the properties file to be created. + * @param foundFiles the list of files to be updated if creation is successful. + */ + private void createFiles(final File file, final List foundFiles) { + File parentDir = file.getParentFile(); + + try { + if (parentDir != null && !parentDir.exists()) { + Files.createDirectories(parentDir.toPath()); + } + + if (!file.exists()) { + Files.createFile(file.toPath()); + foundFiles.add(file); + } + } catch (IOException e) { + createFilesFailed = true; + createFilesFailedCause = file.getAbsolutePath() + " (" + e.toString() + ")"; + } + } } diff --git a/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/RuntimeOptions.java b/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/RuntimeOptions.java index 831128ccbe1..fd4a35d3255 100644 --- a/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/RuntimeOptions.java +++ b/domain-management/src/main/java/org/jboss/as/domain/management/security/adduser/RuntimeOptions.java @@ -32,6 +32,8 @@ class RuntimeOptions { private boolean displaySecret = false; + private boolean createFiles = false; + /** * Enable/Disable mode is active by using --enable or --disable argument */ @@ -129,4 +131,12 @@ boolean isDisplaySecret() { void setDisplaySecret(boolean displaySecret) { this.displaySecret = displaySecret; } + + public boolean isCreateFiles() { + return createFiles; + } + + public void setCreateFiles(boolean createFiles) { + this.createFiles = createFiles; + } } diff --git a/domain-management/src/test/java/org/jboss/as/domain/management/security/adduser/PropertyFileFinderTestCase.java b/domain-management/src/test/java/org/jboss/as/domain/management/security/adduser/PropertyFileFinderTestCase.java index 1bbeac24212..f3a2880f767 100644 --- a/domain-management/src/test/java/org/jboss/as/domain/management/security/adduser/PropertyFileFinderTestCase.java +++ b/domain-management/src/test/java/org/jboss/as/domain/management/security/adduser/PropertyFileFinderTestCase.java @@ -17,11 +17,13 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Properties; import static java.lang.System.getProperty; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; /** * Test the property file finder. @@ -157,5 +159,111 @@ public void noStandaloneDir() throws IOException { assertTrue("Expected the values.getPropertiesFiles() contained the "+domainMgmtUserFile,values.getUserFiles().contains(domainMgmtUserFile)); } + /** + * Tests the creation of parent directories and property files when + * the -cf or --create-files flag is used in Standalone mode configuration. + * + * @throws IOException if an error occurs during directory or file creation. + */ + @Test + public void createFilesServerConfigDir() throws IOException { + RuntimeOptions options = new RuntimeOptions(); + options.setCreateFiles(true); + + File tempDir = new File(System.getProperty("java.io.tmpdir")); + // Using a unique suffix to avoid conflicts during parallel builds + File customDir = new File(tempDir, "user-secrets-test-" + java.util.UUID.randomUUID()); + File customFileMgmtUsers = new File(customDir, "mgmt-users.properties"); + File customFileMgmtGroups = new File(customDir, "mgmt-groups.properties"); + + StateValues stateValues = new StateValues(options); + stateValues.setFileMode(AddUser.FileMode.MANAGEMENT); + stateValues.getOptions().setServerConfigDir(customDir.getAbsolutePath()); + + PropertyFileFinder finder = new PropertyFileFinder(consoleMock, stateValues); + try { + State nextState = finder.execute(); + + assertTrue("Should transition to PromptRealmState", nextState instanceof PromptRealmState); + assertTrue("Directory should have been created", customDir.exists()); + assertTrue("Path should be a directory", customDir.isDirectory()); + assertTrue("User properties file should exist", customFileMgmtUsers.exists()); + assertTrue("Group properties file should exist", customFileMgmtGroups.exists()); + } finally { + if (customDir.exists()) { + cleanFiles(customDir); + } + } + } + + /** + * Tests the creation of parent directories and property files when + * the -cf or --create-files flag is used in Domain mode configuration. + * + * @throws IOException if an error occurs during directory or file creation. + */ + @Test + public void createFilesDomainConfigDir() throws IOException { + RuntimeOptions options = new RuntimeOptions(); + options.setCreateFiles(true); + + File tempDir = new File(System.getProperty("java.io.tmpdir")); + // Using a unique suffix to avoid conflicts during parallel builds + File customDir = new File(tempDir, "user-secrets-test-" + java.util.UUID.randomUUID()); + File customFileMgmtUsers = new File(customDir, "mgmt-users.properties"); + File customFileMgmtGroups = new File(customDir, "mgmt-groups.properties"); + + StateValues stateValues = new StateValues(options); + stateValues.setFileMode(AddUser.FileMode.MANAGEMENT); + stateValues.getOptions().setDomainConfigDir(customDir.getAbsolutePath()); + + PropertyFileFinder finder = new PropertyFileFinder(consoleMock, stateValues); + try { + State nextState = finder.execute(); + assertTrue("Should transition to PromptRealmState", nextState instanceof PromptRealmState); + assertTrue("Directory should have been created", customDir.exists()); + assertTrue("Path should be a directory", customDir.isDirectory()); + assertTrue("User properties file should exist", customFileMgmtUsers.exists()); + assertTrue("Group properties file should exist", customFileMgmtGroups.exists()); + } finally { + if (customDir.exists()) { + cleanFiles(customDir); + } + } + } + + /** + * Tests the error handling when the -cf or --create-files flag is used + * and the application lacks the necessary permissions to create the path. + * + * @throws IOException if an unexpected error occurs. + */ + @Test + public void permissionError() throws IOException { + File readOnlyDir = Files.createTempDirectory("read_only_dir").toFile(); + + try { + readOnlyDir.setWritable(false); + + if (System.getProperty("os.name").toLowerCase().contains("win")) { + assumeFalse("Skipping: Windows ignored setWritable(false) on directories", readOnlyDir.canWrite()); + } + + RuntimeOptions options = new RuntimeOptions(); + options.setCreateFiles(true); + + StateValues stateValues = new StateValues(options); + stateValues.setFileMode(AddUser.FileMode.MANAGEMENT); + stateValues.getOptions().setServerConfigDir(readOnlyDir.getAbsolutePath()); + + PropertyFileFinder finder = new PropertyFileFinder(consoleMock, stateValues); + State nextState = finder.execute(); + + assertTrue("Should return ErrorState due to lack of write permissions", nextState instanceof ErrorState); + } finally { + readOnlyDir.setWritable(true); + cleanFiles(readOnlyDir); + } + } }