diff --git a/docs/_docs/getting-started/configuration.md b/docs/_docs/getting-started/configuration.md index c40b09fcb1..dae94119f3 100644 --- a/docs/_docs/getting-started/configuration.md +++ b/docs/_docs/getting-started/configuration.md @@ -455,6 +455,39 @@ If a port number is specified for a URL, only the requests with that port number Dependency-Track supports proxies that require BASIC, DIGEST, and NTLM authentication. +#### Initial admin user + +By default, Dependency Track will generate an administrative `admin` user with a default set of credentials (`admin:admin`) on first startup. The user is created with `forceChangePassword=true`. +For containerized deployments, you can override this behaviour on first startup using the environment variables below : + +```shell +DEPENDENCY_TRACK_ADMIN_USERNAME +DEPENDENCY_TRACK_ADMIN_PASSWORD +DEPENDENCY_TRACK_ADMIN_FULL_NAME +DEPENDENCY_TRACK_ADMIN_EMAIL +``` + +#### Administrative configuration + +Besides the technical configuration (i.e `application.properties`) described above, Dependency Track allow administrators to configure various part of the application behaviour through the UI. Those configuration items are saved in the database in table `CONFIGPROPERTY`. +On the first startup, this table is loaded with default values that you can find in [ConfigPropertyConstants.java](https://github.com/DependencyTrack/dependency-track/blob/master/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java). + +For containerized deployments, the config properties default values can also be specified as environment variables. All environment variables are based on uppercase property group joined with uppercase property name by an underscore (_). Periods (.) and hyphens (-) replaced with underscores (_). Most of the times, the enumeration name follow this convention. + +For example enumeration + +``` +VULNERABILITY_SOURCE_NVD_ENABLED("vuln-source", "nvd.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable National Vulnerability Database") +``` + +translate to environment variable + +``` +VULN_SOURCE_NVD_ENABLED +``` + +**Please note that the environment variables will be processed only on the first startup**. Once loaded up, the database table `CONFIGPROPERTY` is the single source of truth. + #### Logging Levels Logging levels (INFO, WARN, ERROR, DEBUG, TRACE) can be specified by passing the level diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index b05f92b5b4..4f97bfc0e2 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -28,7 +28,6 @@ import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.License; import org.dependencytrack.model.RepositoryType; -import org.dependencytrack.notification.publisher.DefaultNotificationPublishers; import org.dependencytrack.parser.spdx.json.SpdxLicenseDetailParser; import org.dependencytrack.persistence.defaults.DefaultLicenseGroupImporter; import org.dependencytrack.util.NotificationUtil; @@ -49,6 +48,22 @@ public class DefaultObjectGenerator implements ServletContextListener { private static final Logger LOGGER = Logger.getLogger(DefaultObjectGenerator.class); + static final String DEFAULT_ADMIN_USERNAME = "admin"; + + static final String ADMIN_USERNAME_ENV_VARIABLE = "DEPENDENCY_TRACK_ADMIN_USERNAME"; + + static final String DEFAULT_ADMIN_PASSWORD = "admin"; + + static final String ADMIN_PASSWORD_ENV_VARIABLE = "DEPENDENCY_TRACK_ADMIN_PASSWORD"; + + static final String DEFAULT_ADMIN_FULL_NAME = "Administrator"; + + static final String ADMIN_FULL_NAME_ENV_VARIABLE = "DEPENDENCY_TRACK_ADMIN_FULL_NAME"; + + static final String DEFAULT_ADMIN_EMAIL = "admin@localhost"; + + static final String ADMIN_EMAIL_ENV_VARIABLE = "DEPENDENCY_TRACK_ADMIN_EMAIL"; + /** * {@inheritDoc} */ @@ -148,9 +163,14 @@ private void loadDefaultPersonas() { return; } LOGGER.info("Adding default users and teams to datastore"); - LOGGER.debug("Creating user: admin"); - ManagedUser admin = qm.createManagedUser("admin", "Administrator", "admin@localhost", - new String(PasswordService.createHash("admin".toCharArray())), true, true, false); + String adminUsername = getEnvVariable(ADMIN_USERNAME_ENV_VARIABLE, DEFAULT_ADMIN_USERNAME); + String adminPassword = getEnvVariable(ADMIN_PASSWORD_ENV_VARIABLE, DEFAULT_ADMIN_PASSWORD); + String adminFullName = getEnvVariable(ADMIN_FULL_NAME_ENV_VARIABLE, DEFAULT_ADMIN_FULL_NAME); + String adminEmail = getEnvVariable(ADMIN_EMAIL_ENV_VARIABLE, DEFAULT_ADMIN_EMAIL); + + LOGGER.debug("Creating user: "+adminUsername); + ManagedUser admin = qm.createManagedUser(adminUsername, adminFullName, adminEmail, + new String(PasswordService.createHash(adminPassword.toCharArray())), DEFAULT_ADMIN_PASSWORD.equals(adminPassword), true, false); LOGGER.debug("Creating team: Administrators"); final Team sysadmins = qm.createTeam("Administrators", false); @@ -231,8 +251,9 @@ private void loadDefaultConfigProperties() { LOGGER.info("Synchronizing config properties to datastore"); for (final ConfigPropertyConstants cpc : ConfigPropertyConstants.values()) { LOGGER.debug("Creating config property: " + cpc.getGroupName() + " / " + cpc.getPropertyName()); + if (qm.getConfigProperty(cpc.getGroupName(), cpc.getPropertyName()) == null) { - qm.createConfigProperty(cpc.getGroupName(), cpc.getPropertyName(), cpc.getDefaultPropertyValue(), cpc.getPropertyType(), cpc.getDescription()); + qm.createConfigProperty(cpc.getGroupName(), cpc.getPropertyName(), getEnvVariable(generateEnvVariableName(cpc), cpc.getDefaultPropertyValue()), cpc.getPropertyType(), cpc.getDescription()); } } } @@ -244,13 +265,23 @@ private void loadDefaultConfigProperties() { private void loadDefaultNotificationPublishers() { try (QueryManager qm = new QueryManager()) { LOGGER.info("Synchronizing notification publishers to datastore"); - for (final DefaultNotificationPublishers publisher : DefaultNotificationPublishers.values()) { - try { - NotificationUtil.loadDefaultNotificationPublishers(qm); - } catch (IOException e) { - LOGGER.error("An error occurred while synchronizing a default notification publisher", e); - } - } + NotificationUtil.loadDefaultNotificationPublishers(qm); + } catch (IOException e) { + LOGGER.error("An error occurred while synchronizing a default notification publisher", e); } } + + String generateEnvVariableName(ConfigPropertyConstants configProperty) { + StringBuilder sb = new StringBuilder(); + sb.append(configProperty.getGroupName().toUpperCase().replaceAll("[\\-\\.]", "_")); + sb.append("_"); + sb.append(configProperty.getPropertyName().toUpperCase().replaceAll("[\\-\\.]", "_")); + LOGGER.debug("Environment variable name for property group "+configProperty.getGroupName()+" and property name "+configProperty.getPropertyName()+" is "+sb); + return sb.toString(); + } + + String getEnvVariable(String name, String defaultValue) { + String value = System.getenv(name); + return value != null ? value : defaultValue; + } } diff --git a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java index 72fc588653..e540980287 100644 --- a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java +++ b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java @@ -18,6 +18,8 @@ */ package org.dependencytrack.persistence; +import alpine.model.ConfigProperty; +import alpine.server.auth.PasswordService; import org.dependencytrack.PersistenceCapableTest; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.ConfigPropertyConstants; @@ -25,7 +27,9 @@ import org.junit.Assert; import org.junit.Test; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.Map; public class DefaultObjectGeneratorTest extends PersistenceCapableTest { @@ -60,10 +64,39 @@ public void testLoadDefaultPermissions() throws Exception { @Test public void testLoadDefaultPersonas() throws Exception { DefaultObjectGenerator generator = new DefaultObjectGenerator(); + clearEnvironmentVariable(DefaultObjectGenerator.ADMIN_USERNAME_ENV_VARIABLE); + clearEnvironmentVariable(DefaultObjectGenerator.ADMIN_PASSWORD_ENV_VARIABLE); + clearEnvironmentVariable(DefaultObjectGenerator.ADMIN_FULL_NAME_ENV_VARIABLE); + clearEnvironmentVariable(DefaultObjectGenerator.ADMIN_EMAIL_ENV_VARIABLE); Method method = generator.getClass().getDeclaredMethod("loadDefaultPersonas"); method.setAccessible(true); method.invoke(generator); Assert.assertEquals(3, qm.getTeams().size()); + var users = qm.getManagedUsers(); + Assert.assertEquals(1, users.size()); + Assert.assertEquals(DefaultObjectGenerator.DEFAULT_ADMIN_USERNAME, users.get(0).getUsername()); + Assert.assertTrue(PasswordService.matches(DefaultObjectGenerator.DEFAULT_ADMIN_PASSWORD.toCharArray(), users.get(0))); + Assert.assertEquals(DefaultObjectGenerator.DEFAULT_ADMIN_FULL_NAME, users.get(0).getFullname()); + Assert.assertEquals(DefaultObjectGenerator.DEFAULT_ADMIN_EMAIL, users.get(0).getEmail()); + } + + @Test + public void testLoadDefaultPersonasWithUserProvidedCredentials() throws Exception { + DefaultObjectGenerator generator = new DefaultObjectGenerator(); + setEnvironmentVariable(DefaultObjectGenerator.ADMIN_USERNAME_ENV_VARIABLE, "test"); + setEnvironmentVariable(DefaultObjectGenerator.ADMIN_PASSWORD_ENV_VARIABLE, "testPassword"); + setEnvironmentVariable(DefaultObjectGenerator.ADMIN_FULL_NAME_ENV_VARIABLE, "test test"); + setEnvironmentVariable(DefaultObjectGenerator.ADMIN_EMAIL_ENV_VARIABLE, "test@test.dev"); + Method method = generator.getClass().getDeclaredMethod("loadDefaultPersonas"); + method.setAccessible(true); + method.invoke(generator); + Assert.assertEquals(3, qm.getTeams().size()); + var users = qm.getManagedUsers(); + Assert.assertEquals(1, users.size()); + Assert.assertEquals("test", users.get(0).getUsername()); + Assert.assertTrue(PasswordService.matches("testPassword".toCharArray(), users.get(0))); + Assert.assertEquals("test test", users.get(0).getFullname()); + Assert.assertEquals("test@test.dev", users.get(0).getEmail()); } @Test @@ -78,10 +111,14 @@ public void testLoadDefaultRepositories() throws Exception { @Test public void testLoadDefaultConfigProperties() throws Exception { DefaultObjectGenerator generator = new DefaultObjectGenerator(); + String nvdToggleEnvVariableName = generator.generateEnvVariableName(ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_ENABLED); + setEnvironmentVariable(nvdToggleEnvVariableName, "false"); Method method = generator.getClass().getDeclaredMethod("loadDefaultConfigProperties"); method.setAccessible(true); method.invoke(generator); Assert.assertEquals(ConfigPropertyConstants.values().length, qm.getConfigProperties().size()); + ConfigProperty nvdEnabled = qm.getConfigProperty(ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_ENABLED.getGroupName(), ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_ENABLED.getPropertyName()); + Assert.assertEquals("false", nvdEnabled.getPropertyValue()); } @Test @@ -92,4 +129,63 @@ public void testLoadDefaultNotificationPublishers() throws Exception { method.invoke(generator); Assert.assertEquals(DefaultNotificationPublishers.values().length, qm.getAllNotificationPublishers().size()); } + + private static void clearEnvironmentVariable(String key) throws Exception { + Class processEnvironment = Class.forName("java.lang.ProcessEnvironment"); + + Field unmodifiableMapField = getAccessibleField(processEnvironment, "theUnmodifiableEnvironment"); + Object unmodifiableMap = unmodifiableMapField.get(null); + clearUnmodifiableMap(key, unmodifiableMap); + + Field caseInsensitiveMapField = getAccessibleField(processEnvironment, "theCaseInsensitiveEnvironment"); + Map caseInsensitiveMap = (Map) caseInsensitiveMapField.get(null); + caseInsensitiveMap.remove(key); + + Field mapField = getAccessibleField(processEnvironment, "theEnvironment"); + Map map = (Map) mapField.get(null); + map.remove(key); + } + + private static void setEnvironmentVariable(String key, String value) throws Exception { + + Class processEnvironment = Class.forName("java.lang.ProcessEnvironment"); + + Field unmodifiableMapField = getAccessibleField(processEnvironment, "theUnmodifiableEnvironment"); + Object unmodifiableMap = unmodifiableMapField.get(null); + injectIntoUnmodifiableMap(key, value, unmodifiableMap); + + Field caseInsensitiveMapField = getAccessibleField(processEnvironment, "theCaseInsensitiveEnvironment"); + Map caseInsensitiveMap = (Map) caseInsensitiveMapField.get(null); + caseInsensitiveMap.put(key, value); + + Field mapField = getAccessibleField(processEnvironment, "theEnvironment"); + Map map = (Map) mapField.get(null); + map.put(key, value); + } + + private static Field getAccessibleField(Class clazz, String fieldName) + throws NoSuchFieldException { + + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } + + private static void injectIntoUnmodifiableMap(String key, String value, Object map) + throws ReflectiveOperationException { + + Class unmodifiableMap = Class.forName("java.util.Collections$UnmodifiableMap"); + Field field = getAccessibleField(unmodifiableMap, "m"); + Object obj = field.get(map); + ((Map) obj).put(key, value); + } + + private static void clearUnmodifiableMap(String key, Object map) + throws ReflectiveOperationException { + + Class unmodifiableMap = Class.forName("java.util.Collections$UnmodifiableMap"); + Field field = getAccessibleField(unmodifiableMap, "m"); + Object obj = field.get(map); + ((Map) obj).remove(key); + } }