Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 41 additions & 43 deletions api/src/org/labkey/api/module/ModuleLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -2561,67 +2561,65 @@ public FileLike getStartupPropDirectory()
private void loadStartupProps()
{
FileLike propsDir = getStartupPropDirectory();
if (null == propsDir)
return;

if (!propsDir.isDirectory())
return;

FileLike newinstall = propsDir.resolveChild("newinstall");
if (newinstall.isFile())
if (null != propsDir && propsDir.isDirectory())
{
_log.debug("'newinstall' file detected: {}", newinstall.toNioPathForRead());
FileLike newinstall = propsDir.resolveChild("newinstall");
if (newinstall.isFile())
{
_log.debug("'newinstall' file detected: {}", newinstall.toNioPathForRead());

_newInstall = true;
_newInstall = true;

// propsDir is readonly, so we need to cheat to get a File
var newInstallFile = newinstall.toNioPathForRead().toFile();
if (newInstallFile.canWrite())
newInstallFile.delete();
// propsDir is readonly, so we need to cheat to get a File
var newInstallFile = newinstall.toNioPathForRead().toFile();
if (newInstallFile.canWrite())
newInstallFile.delete();
else
throw new ConfigurationException("file 'newinstall' exists, but is not writeable: " + newinstall.toNioPathForRead());
}
else
throw new ConfigurationException("file 'newinstall' exists, but is not writeable: " + newinstall.toNioPathForRead());
}
else
{
_log.debug("no 'newinstall' file detected");
}

List<FileLike> propFiles = propsDir.getChildren().stream().filter(f -> f.getName().endsWith(".properties")).toList();
{
_log.debug("no 'newinstall' file detected");
}

if (!propFiles.isEmpty())
{
List<FileLike> sortedPropFiles = propFiles.stream()
.sorted(Comparator.comparing(FileLike::getName).reversed())
.toList();
List<FileLike> propFiles = propsDir.getChildren().stream().filter(f -> f.getName().endsWith(".properties")).toList();

for (FileLike propFile : sortedPropFiles)
if (!propFiles.isEmpty())
{
_log.debug("loading propsFile: {}", propFile.toNioPathForRead());
List<FileLike> sortedPropFiles = propFiles.stream()
.sorted(Comparator.comparing(FileLike::getName).reversed())
.toList();

try (InputStream in = propFile.openInputStream())
for (FileLike propFile : sortedPropFiles)
{
Properties props = new Properties();
props.load(in);
_log.debug("loading propsFile: {}", propFile.toNioPathForRead());

for (Map.Entry<Object, Object> entry : props.entrySet())
try (InputStream in = propFile.openInputStream())
{
if (entry.getKey() instanceof String && entry.getValue() instanceof String)
Properties props = new Properties();
props.load(in);

for (Map.Entry<Object, Object> entry : props.entrySet())
{
_log.trace("property '{}' resolved to value: '{}'", entry.getKey(), entry.getValue());
if (entry.getKey() instanceof String && entry.getValue() instanceof String)
{
_log.trace("property '{}' resolved to value: '{}'", entry.getKey(), entry.getValue());

addStartupPropertyEntry(entry.getKey().toString(), entry.getValue().toString());
addStartupPropertyEntry(entry.getKey().toString(), entry.getValue().toString());
}
}
}
}
catch (Exception e)
{
_log.error("Error parsing startup config properties file '{}'", propFile.toNioPathForRead(), e);
catch (Exception e)
{
_log.error("Error parsing startup config properties file '{}'", propFile.toNioPathForRead(), e);
}
}
}
}
else
{
_log.debug("no propFiles to load");
else
{
_log.debug("no propFiles to load");
}
}

// load any system properties with the labkey prop prefix
Expand Down
5 changes: 3 additions & 2 deletions api/src/org/labkey/api/reports/ExternalScriptEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.labkey.api.reader.Readers;
import org.labkey.api.reports.report.r.ParamReplacementSvc;
import org.labkey.api.util.ExceptionUtil;
import org.labkey.api.util.LabKeyProcessBuilder;
import org.labkey.api.util.FileUtil;
import org.labkey.api.util.QuietCloser;
import org.labkey.api.util.URIUtil;
Expand Down Expand Up @@ -126,7 +127,7 @@ public Object eval(String script, ScriptContext context) throws ScriptException
protected Object eval(FileLike scriptFile, ScriptContext context) throws ScriptException
{
String[] params = formatCommand(scriptFile, context);
ProcessBuilder pb = new ProcessBuilder(params);
LabKeyProcessBuilder pb = new LabKeyProcessBuilder(params);
pb = pb.directory(getWorkingDir(context).toNioPathForRead().toFile());

final long timeout = getTimeout(context);
Expand Down Expand Up @@ -318,7 +319,7 @@ else if (value.startsWith("\""))
* Execute the external script engine in separate process
* @return the exit code for the invocation - 0 if the process completed successfully.
*/
protected int runProcess(ScriptContext context, ProcessBuilder pb, StringBuffer output, long timeout, TimeUnit timeoutUnit)
protected int runProcess(ScriptContext context, LabKeyProcessBuilder pb, StringBuffer output, long timeout, TimeUnit timeoutUnit)
{
Process proc;
try
Expand Down
41 changes: 41 additions & 0 deletions api/src/org/labkey/api/secrets/SecretProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.labkey.api.secrets;

import org.labkey.api.settings.StartupProperty;

/**
* Describes a named secret that a module needs to access. Register instances with
* {@link SecretService#register} during module startup; retrieve values via
* {@link SecretService#getSecret}.
*
* Startup property file convention: {@code secret.<propertyName>=<value>}
* Java property convention: {@code -Plabkey.prop.secret.<propertyName>=<value>}
* Environment variable convention: {@code export <propertyName>=<value>}
*/
public class SecretProperty implements StartupProperty
{
private final String _name;
private final String _description;

public SecretProperty(String name)
{
this(name, "Secret: " + name);
}

public SecretProperty(String name, String description)
{
_name = name;
_description = description;
}

@Override
public String getPropertyName()
{
return _name;
}

@Override
public String getDescription()
{
return _description;
}
}
18 changes: 18 additions & 0 deletions api/src/org/labkey/api/secrets/SecretProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.labkey.api.secrets;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* SPI for a secret source. Implementations cover built-in sources (startup property files,
* environment variables) and external stores (e.g., AWS SSM Parameter Store).
* Providers are consulted in priority order by {@link SecretService}.
*/
public interface SecretProvider
{
/** Returns the secret for the given property name, or null if not available from this source. */
@Nullable String getSecret(String propertyName);

/** Human-readable name for this source, shown on the admin secrets page. */
@NotNull String getDescription();
}
78 changes: 78 additions & 0 deletions api/src/org/labkey/api/secrets/SecretService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.labkey.api.secrets;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.services.ServiceRegistry;

import java.util.List;

/**
* Internal service that provides access to secrets (API keys, passwords, etc.) without
* requiring callers to know where the secret is stored. Secrets may come from startup
* property files, process environment variables, or an external store such as AWS SSM.
*
* <p>Modules should:
* <ol>
* <li>Declare each secret as a {@code static final SecretProperty} constant.</li>
* <li>Call {@link #register} in their {@code doStartup()} method.</li>
* <li>Call {@link #getSecret} wherever the value is needed.</li>
* </ol>
*
* <p>Startup property file convention: {@code secret.<propertyName>=<value>}
*/
public interface SecretService
{
static @NotNull SecretService get()
{
SecretService svc = ServiceRegistry.get().getService(SecretService.class);
if (svc == null)
throw new IllegalStateException("SecretService has not been initialized");
return svc;
}

static void setInstance(SecretService service)
{
ServiceRegistry.get().registerService(SecretService.class, service);
}

/**
* Declare that the calling module may request the named secret. Should be called
* from {@code Module.doStartup()}. Registration is for documentation and filtering
* (e.g., admin env-var page redaction); it does not affect whether a value is returned.
*/
void register(@NotNull SecretProperty property);

/**
* Retrieve the value of a secret. Returns {@code null} if the secret has not been
* configured in any source. Never logs or caches the returned value.
*
* <p><strong>Identity contract:</strong> the {@code property} argument must be the exact
* {@code static final} instance that was passed to {@link #register}. A freshly constructed
* {@code new SecretProperty("SOME_KEY")} will always return {@code null}, even if a secret
* with that name is configured. This prevents unregistered callers from reading secrets
* they did not declare.
*/
@Nullable String getSecret(@NotNull SecretProperty property);

/** Returns true if the given property name has been registered via {@link #register}. */
boolean isRegisteredSecret(@NotNull String name);

/**
* Register a high-priority {@link SecretProvider} (e.g., AWS SSM). This provider is
* consulted before the built-in startup-property and environment-variable providers.
*/
void setExternalProvider(@NotNull SecretProvider provider);

/**
* Returns read-only status for every registered secret, sorted by name.
* Never includes secret values — safe to display in admin UI.
*/
@NotNull List<SecretStatus> getSecretStatuses();

/**
* Returns a human-readable description of the active external provider (e.g.,
* "AWS SSM Parameter Store"), or {@code null} if no external provider is registered.
* The external provider takes priority over startup-property and environment-variable sources.
*/
@Nullable String getExternalProviderDescription();
}
19 changes: 19 additions & 0 deletions api/src/org/labkey/api/secrets/SecretStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.labkey.api.secrets;

import org.jetbrains.annotations.Nullable;

/**
* Read-only status of a registered secret — suitable for admin UI display.
* Never contains the secret value itself.
*
* @param source description of the provider that holds this secret
* (e.g. "Startup property file", "Environment variable"), or
* {@code null} if no provider has a value for it
*/
public record SecretStatus(String name, String description, @Nullable String source)
{
public boolean isSet()
{
return source != null;
}
}
4 changes: 2 additions & 2 deletions api/src/org/labkey/api/util/GUID.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ private static String networkIdentifier()

try
{
ProcessBuilder cmd = new ProcessBuilder("ipconfig.exe", "/all");
LabKeyProcessBuilder cmd = new LabKeyProcessBuilder("ipconfig.exe", "/all");
cmd.redirectErrorStream(true);
p = cmd.start();
}
Expand All @@ -135,7 +135,7 @@ private static String networkIdentifier()
{
try
{
ProcessBuilder cmd = new ProcessBuilder("ifconfig", "-a");
LabKeyProcessBuilder cmd = new LabKeyProcessBuilder("ifconfig", "-a");
cmd.redirectErrorStream(true);
p = cmd.start();
}
Expand Down
Loading