Skip to content

Commit

Permalink
Feature: Improve automated containerized deployment
Browse files Browse the repository at this point in the history
See #2443. Allow configuration by environment variables.

Signed-off-by: syalioune <[email protected]>
  • Loading branch information
syalioune committed Mar 2, 2023
1 parent 90a1967 commit 3af4dce
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 12 deletions.
33 changes: 33 additions & 0 deletions docs/_docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}
*/
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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());
}
}
}
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@
*/
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;
import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
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 {

Expand Down Expand Up @@ -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, "[email protected]");
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("[email protected]", users.get(0).getEmail());
}

@Test
Expand All @@ -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
Expand All @@ -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<String, String> caseInsensitiveMap = (Map<String, String>) caseInsensitiveMapField.get(null);
caseInsensitiveMap.remove(key);

Field mapField = getAccessibleField(processEnvironment, "theEnvironment");
Map<String, String> map = (Map<String, String>) 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<String, String> caseInsensitiveMap = (Map<String, String>) caseInsensitiveMapField.get(null);
caseInsensitiveMap.put(key, value);

Field mapField = getAccessibleField(processEnvironment, "theEnvironment");
Map<String, String> map = (Map<String, String>) 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<String, String>) 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<String, String>) obj).remove(key);
}
}

0 comments on commit 3af4dce

Please sign in to comment.